Merge branch 'develop' into core
This commit is contained in:
commit
c7f4a8fd39
@ -92,15 +92,11 @@ class GraphicsScene(QtGui.QGraphicsScene):
|
|||||||
|
|
||||||
self.clickEvents = []
|
self.clickEvents = []
|
||||||
self.dragButtons = []
|
self.dragButtons = []
|
||||||
self.prepItems = weakref.WeakKeyDictionary() ## set of items with prepareForPaintMethods
|
|
||||||
self.mouseGrabber = None
|
self.mouseGrabber = None
|
||||||
self.dragItem = None
|
self.dragItem = None
|
||||||
self.lastDrag = None
|
self.lastDrag = None
|
||||||
self.hoverItems = weakref.WeakKeyDictionary()
|
self.hoverItems = weakref.WeakKeyDictionary()
|
||||||
self.lastHoverEvent = None
|
self.lastHoverEvent = None
|
||||||
#self.searchRect = QtGui.QGraphicsRectItem()
|
|
||||||
#self.searchRect.setPen(fn.mkPen(200,0,0))
|
|
||||||
#self.addItem(self.searchRect)
|
|
||||||
|
|
||||||
self.contextMenu = [QtGui.QAction("Export...", self)]
|
self.contextMenu = [QtGui.QAction("Export...", self)]
|
||||||
self.contextMenu[0].triggered.connect(self.showExportDialog)
|
self.contextMenu[0].triggered.connect(self.showExportDialog)
|
||||||
@ -437,10 +433,10 @@ class GraphicsScene(QtGui.QGraphicsScene):
|
|||||||
for item in items:
|
for item in items:
|
||||||
if hoverable and not hasattr(item, 'hoverEvent'):
|
if hoverable and not hasattr(item, 'hoverEvent'):
|
||||||
continue
|
continue
|
||||||
shape = item.shape()
|
shape = item.shape() # Note: default shape() returns boundingRect()
|
||||||
if shape is None:
|
if shape is None:
|
||||||
continue
|
continue
|
||||||
if item.mapToScene(shape).contains(point):
|
if shape.contains(item.mapFromScene(point)):
|
||||||
items2.append(item)
|
items2.append(item)
|
||||||
|
|
||||||
## Sort by descending Z-order (don't trust scene.itms() to do this either)
|
## Sort by descending Z-order (don't trust scene.itms() to do this either)
|
||||||
|
@ -131,8 +131,12 @@ class MouseDragEvent(object):
|
|||||||
return self.finish
|
return self.finish
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
lp = self.lastPos()
|
if self.currentItem is None:
|
||||||
p = self.pos()
|
lp = self._lastScenePos
|
||||||
|
p = self._scenePos
|
||||||
|
else:
|
||||||
|
lp = self.lastPos()
|
||||||
|
p = self.pos()
|
||||||
return "<MouseDragEvent (%g,%g)->(%g,%g) buttons=%d start=%s finish=%s>" % (lp.x(), lp.y(), p.x(), p.y(), int(self.buttons()), str(self.isStart()), str(self.isFinish()))
|
return "<MouseDragEvent (%g,%g)->(%g,%g) buttons=%d start=%s finish=%s>" % (lp.x(), lp.y(), p.x(), p.y(), int(self.buttons()), str(self.isStart()), str(self.isFinish()))
|
||||||
|
|
||||||
def modifiers(self):
|
def modifiers(self):
|
||||||
@ -221,9 +225,15 @@ class MouseClickEvent(object):
|
|||||||
return self._modifiers
|
return self._modifiers
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
p = self.pos()
|
try:
|
||||||
return "<MouseClickEvent (%g,%g) button=%d>" % (p.x(), p.y(), int(self.button()))
|
if self.currentItem is None:
|
||||||
|
p = self._scenePos
|
||||||
|
else:
|
||||||
|
p = self.pos()
|
||||||
|
return "<MouseClickEvent (%g,%g) button=%d>" % (p.x(), p.y(), int(self.button()))
|
||||||
|
except:
|
||||||
|
return "<MouseClickEvent button=%d>" % (int(self.button()))
|
||||||
|
|
||||||
def time(self):
|
def time(self):
|
||||||
return self._time
|
return self._time
|
||||||
|
|
||||||
@ -345,8 +355,12 @@ class HoverEvent(object):
|
|||||||
return Point(self.currentItem.mapFromScene(self._lastScenePos))
|
return Point(self.currentItem.mapFromScene(self._lastScenePos))
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
lp = self.lastPos()
|
if self.currentItem is None:
|
||||||
p = self.pos()
|
lp = self._lastScenePos
|
||||||
|
p = self._scenePos
|
||||||
|
else:
|
||||||
|
lp = self.lastPos()
|
||||||
|
p = self.pos()
|
||||||
return "<HoverEvent (%g,%g)->(%g,%g) buttons=%d enter=%s exit=%s>" % (lp.x(), lp.y(), p.x(), p.y(), int(self.buttons()), str(self.isEnter()), str(self.isExit()))
|
return "<HoverEvent (%g,%g)->(%g,%g) buttons=%d enter=%s exit=%s>" % (lp.x(), lp.y(), p.x(), p.y(), int(self.buttons()), str(self.isEnter()), str(self.isExit()))
|
||||||
|
|
||||||
def modifiers(self):
|
def modifiers(self):
|
||||||
|
20
Qt.py
20
Qt.py
@ -32,6 +32,23 @@ else:
|
|||||||
if USE_PYSIDE:
|
if USE_PYSIDE:
|
||||||
from PySide import QtGui, QtCore, QtOpenGL, QtSvg
|
from PySide import QtGui, QtCore, QtOpenGL, QtSvg
|
||||||
import PySide
|
import PySide
|
||||||
|
try:
|
||||||
|
from PySide import shiboken
|
||||||
|
isQObjectAlive = shiboken.isValid
|
||||||
|
except ImportError:
|
||||||
|
def isQObjectAlive(obj):
|
||||||
|
try:
|
||||||
|
if hasattr(obj, 'parent'):
|
||||||
|
obj.parent()
|
||||||
|
elif hasattr(obj, 'parentItem'):
|
||||||
|
obj.parentItem()
|
||||||
|
else:
|
||||||
|
raise Exception("Cannot determine whether Qt object %s is still alive." % obj)
|
||||||
|
except RuntimeError:
|
||||||
|
return False
|
||||||
|
else:
|
||||||
|
return True
|
||||||
|
|
||||||
VERSION_INFO = 'PySide ' + PySide.__version__
|
VERSION_INFO = 'PySide ' + PySide.__version__
|
||||||
|
|
||||||
# Make a loadUiType function like PyQt has
|
# Make a loadUiType function like PyQt has
|
||||||
@ -78,6 +95,9 @@ else:
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
import sip
|
||||||
|
def isQObjectAlive(obj):
|
||||||
|
return not sip.isdeleted(obj)
|
||||||
loadUiType = uic.loadUiType
|
loadUiType = uic.loadUiType
|
||||||
|
|
||||||
QtCore.Signal = QtCore.pyqtSignal
|
QtCore.Signal = QtCore.pyqtSignal
|
||||||
|
@ -4,7 +4,6 @@ from .Vector import Vector
|
|||||||
from .Transform3D import Transform3D
|
from .Transform3D import Transform3D
|
||||||
from .Vector import Vector
|
from .Vector import Vector
|
||||||
import numpy as np
|
import numpy as np
|
||||||
import scipy.linalg
|
|
||||||
|
|
||||||
class SRTTransform3D(Transform3D):
|
class SRTTransform3D(Transform3D):
|
||||||
"""4x4 Transform matrix that can always be represented as a combination of 3 matrices: scale * rotate * translate
|
"""4x4 Transform matrix that can always be represented as a combination of 3 matrices: scale * rotate * translate
|
||||||
@ -118,11 +117,13 @@ class SRTTransform3D(Transform3D):
|
|||||||
The input matrix must be affine AND have no shear,
|
The input matrix must be affine AND have no shear,
|
||||||
otherwise the conversion will most likely fail.
|
otherwise the conversion will most likely fail.
|
||||||
"""
|
"""
|
||||||
|
import numpy.linalg
|
||||||
for i in range(4):
|
for i in range(4):
|
||||||
self.setRow(i, m.row(i))
|
self.setRow(i, m.row(i))
|
||||||
m = self.matrix().reshape(4,4)
|
m = self.matrix().reshape(4,4)
|
||||||
## translation is 4th column
|
## translation is 4th column
|
||||||
self._state['pos'] = m[:3,3]
|
self._state['pos'] = m[:3,3]
|
||||||
|
|
||||||
## scale is vector-length of first three columns
|
## scale is vector-length of first three columns
|
||||||
scale = (m[:3,:3]**2).sum(axis=0)**0.5
|
scale = (m[:3,:3]**2).sum(axis=0)**0.5
|
||||||
## see whether there is an inversion
|
## see whether there is an inversion
|
||||||
@ -132,9 +133,9 @@ class SRTTransform3D(Transform3D):
|
|||||||
self._state['scale'] = scale
|
self._state['scale'] = scale
|
||||||
|
|
||||||
## rotation axis is the eigenvector with eigenvalue=1
|
## rotation axis is the eigenvector with eigenvalue=1
|
||||||
r = m[:3, :3] / scale[:, np.newaxis]
|
r = m[:3, :3] / scale[np.newaxis, :]
|
||||||
try:
|
try:
|
||||||
evals, evecs = scipy.linalg.eig(r)
|
evals, evecs = numpy.linalg.eig(r)
|
||||||
except:
|
except:
|
||||||
print("Rotation matrix: %s" % str(r))
|
print("Rotation matrix: %s" % str(r))
|
||||||
print("Scale: %s" % str(scale))
|
print("Scale: %s" % str(scale))
|
||||||
|
15
Vector.py
15
Vector.py
@ -67,4 +67,19 @@ class Vector(QtGui.QVector3D):
|
|||||||
yield(self.x())
|
yield(self.x())
|
||||||
yield(self.y())
|
yield(self.y())
|
||||||
yield(self.z())
|
yield(self.z())
|
||||||
|
|
||||||
|
def angle(self, a):
|
||||||
|
"""Returns the angle in degrees between this vector and the vector a."""
|
||||||
|
n1 = self.length()
|
||||||
|
n2 = a.length()
|
||||||
|
if n1 == 0. or n2 == 0.:
|
||||||
|
return None
|
||||||
|
## Probably this should be done with arctan2 instead..
|
||||||
|
ang = np.arccos(np.clip(QtGui.QVector3D.dotProduct(self, a) / (n1 * n2), -1.0, 1.0)) ### in radians
|
||||||
|
# c = self.crossProduct(a)
|
||||||
|
# if c > 0:
|
||||||
|
# ang *= -1.
|
||||||
|
return ang * 180. / np.pi
|
||||||
|
|
||||||
|
|
||||||
|
|
12
__init__.py
12
__init__.py
@ -52,10 +52,11 @@ CONFIG_OPTIONS = {
|
|||||||
'background': 'k', ## default background for GraphicsWidget
|
'background': 'k', ## default background for GraphicsWidget
|
||||||
'antialias': False,
|
'antialias': False,
|
||||||
'editorCommand': None, ## command used to invoke code editor from ConsoleWidgets
|
'editorCommand': None, ## command used to invoke code editor from ConsoleWidgets
|
||||||
'useWeave': True, ## Use weave to speed up some operations, if it is available
|
'useWeave': False, ## Use weave to speed up some operations, if it is available
|
||||||
'weaveDebug': False, ## Print full error message if weave compile fails
|
'weaveDebug': False, ## Print full error message if weave compile fails
|
||||||
'exitCleanup': True, ## Attempt to work around some exit crash bugs in PyQt and PySide
|
'exitCleanup': True, ## Attempt to work around some exit crash bugs in PyQt and PySide
|
||||||
'enableExperimental': False, ## Enable experimental features (the curious can search for this key in the code)
|
'enableExperimental': False, ## Enable experimental features (the curious can search for this key in the code)
|
||||||
|
'crashWarning': False, # If True, print warnings about situations that may result in a crash
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -256,6 +257,7 @@ from .graphicsWindows import *
|
|||||||
from .SignalProxy import *
|
from .SignalProxy import *
|
||||||
from .colormap import *
|
from .colormap import *
|
||||||
from .ptime import time
|
from .ptime import time
|
||||||
|
from pyqtgraph.Qt import isQObjectAlive
|
||||||
|
|
||||||
|
|
||||||
##############################################################
|
##############################################################
|
||||||
@ -284,7 +286,12 @@ def cleanup():
|
|||||||
s = QtGui.QGraphicsScene()
|
s = QtGui.QGraphicsScene()
|
||||||
for o in gc.get_objects():
|
for o in gc.get_objects():
|
||||||
try:
|
try:
|
||||||
if isinstance(o, QtGui.QGraphicsItem) and o.scene() is None:
|
if isinstance(o, QtGui.QGraphicsItem) and isQObjectAlive(o) and o.scene() is None:
|
||||||
|
if getConfigOption('crashWarning'):
|
||||||
|
sys.stderr.write('Error: graphics item without scene. '
|
||||||
|
'Make sure ViewBox.close() and GraphicsView.close() '
|
||||||
|
'are properly called before app shutdown (%s)\n' % (o,))
|
||||||
|
|
||||||
s.addItem(o)
|
s.addItem(o)
|
||||||
except RuntimeError: ## occurs if a python wrapper no longer has its underlying C++ object
|
except RuntimeError: ## occurs if a python wrapper no longer has its underlying C++ object
|
||||||
continue
|
continue
|
||||||
@ -393,6 +400,7 @@ def dbg(*args, **kwds):
|
|||||||
consoles.append(c)
|
consoles.append(c)
|
||||||
except NameError:
|
except NameError:
|
||||||
consoles = [c]
|
consoles = [c]
|
||||||
|
return c
|
||||||
|
|
||||||
|
|
||||||
def mkQApp():
|
def mkQApp():
|
||||||
|
@ -431,9 +431,12 @@ class CanvasItem(QtCore.QObject):
|
|||||||
def selectionChanged(self, sel, multi):
|
def selectionChanged(self, sel, multi):
|
||||||
"""
|
"""
|
||||||
Inform the item that its selection state has changed.
|
Inform the item that its selection state has changed.
|
||||||
Arguments:
|
============== =========================================================
|
||||||
sel: bool, whether the item is currently selected
|
**Arguments:**
|
||||||
multi: bool, whether there are multiple items currently selected
|
sel (bool) whether the item is currently selected
|
||||||
|
multi (bool) whether there are multiple items currently
|
||||||
|
selected
|
||||||
|
============== =========================================================
|
||||||
"""
|
"""
|
||||||
self.selectedAlone = sel and not multi
|
self.selectedAlone = sel and not multi
|
||||||
self.showSelectBox()
|
self.showSelectBox()
|
||||||
|
68
colormap.py
68
colormap.py
@ -1,5 +1,4 @@
|
|||||||
import numpy as np
|
import numpy as np
|
||||||
import scipy.interpolate
|
|
||||||
from .Qt import QtGui, QtCore
|
from .Qt import QtGui, QtCore
|
||||||
|
|
||||||
class ColorMap(object):
|
class ColorMap(object):
|
||||||
@ -52,20 +51,20 @@ class ColorMap(object):
|
|||||||
|
|
||||||
def __init__(self, pos, color, mode=None):
|
def __init__(self, pos, color, mode=None):
|
||||||
"""
|
"""
|
||||||
========= ==============================================================
|
=============== ==============================================================
|
||||||
Arguments
|
**Arguments:**
|
||||||
pos Array of positions where each color is defined
|
pos Array of positions where each color is defined
|
||||||
color Array of RGBA colors.
|
color Array of RGBA colors.
|
||||||
Integer data types are interpreted as 0-255; float data types
|
Integer data types are interpreted as 0-255; float data types
|
||||||
are interpreted as 0.0-1.0
|
are interpreted as 0.0-1.0
|
||||||
mode Array of color modes (ColorMap.RGB, HSV_POS, or HSV_NEG)
|
mode Array of color modes (ColorMap.RGB, HSV_POS, or HSV_NEG)
|
||||||
indicating the color space that should be used when
|
indicating the color space that should be used when
|
||||||
interpolating between stops. Note that the last mode value is
|
interpolating between stops. Note that the last mode value is
|
||||||
ignored. By default, the mode is entirely RGB.
|
ignored. By default, the mode is entirely RGB.
|
||||||
========= ==============================================================
|
=============== ==============================================================
|
||||||
"""
|
"""
|
||||||
self.pos = pos
|
self.pos = np.array(pos)
|
||||||
self.color = color
|
self.color = np.array(color)
|
||||||
if mode is None:
|
if mode is None:
|
||||||
mode = np.ones(len(pos))
|
mode = np.ones(len(pos))
|
||||||
self.mode = mode
|
self.mode = mode
|
||||||
@ -92,15 +91,24 @@ class ColorMap(object):
|
|||||||
else:
|
else:
|
||||||
pos, color = self.getStops(mode)
|
pos, color = self.getStops(mode)
|
||||||
|
|
||||||
data = np.clip(data, pos.min(), pos.max())
|
# don't need this--np.interp takes care of it.
|
||||||
|
#data = np.clip(data, pos.min(), pos.max())
|
||||||
|
|
||||||
if not isinstance(data, np.ndarray):
|
# Interpolate
|
||||||
interp = scipy.interpolate.griddata(pos, color, np.array([data]))[0]
|
# TODO: is griddata faster?
|
||||||
|
# interp = scipy.interpolate.griddata(pos, color, data)
|
||||||
|
if np.isscalar(data):
|
||||||
|
interp = np.empty((color.shape[1],), dtype=color.dtype)
|
||||||
else:
|
else:
|
||||||
interp = scipy.interpolate.griddata(pos, color, data)
|
|
||||||
|
|
||||||
if mode == self.QCOLOR:
|
|
||||||
if not isinstance(data, np.ndarray):
|
if not isinstance(data, np.ndarray):
|
||||||
|
data = np.array(data)
|
||||||
|
interp = np.empty(data.shape + (color.shape[1],), dtype=color.dtype)
|
||||||
|
for i in range(color.shape[1]):
|
||||||
|
interp[...,i] = np.interp(data, pos, color[:,i])
|
||||||
|
|
||||||
|
# Convert to QColor if requested
|
||||||
|
if mode == self.QCOLOR:
|
||||||
|
if np.isscalar(data):
|
||||||
return QtGui.QColor(*interp)
|
return QtGui.QColor(*interp)
|
||||||
else:
|
else:
|
||||||
return [QtGui.QColor(*x) for x in interp]
|
return [QtGui.QColor(*x) for x in interp]
|
||||||
@ -193,16 +201,16 @@ class ColorMap(object):
|
|||||||
"""
|
"""
|
||||||
Return an RGB(A) lookup table (ndarray).
|
Return an RGB(A) lookup table (ndarray).
|
||||||
|
|
||||||
============= ============================================================================
|
=============== =============================================================================
|
||||||
**Arguments**
|
**Arguments:**
|
||||||
start The starting value in the lookup table (default=0.0)
|
start The starting value in the lookup table (default=0.0)
|
||||||
stop The final value in the lookup table (default=1.0)
|
stop The final value in the lookup table (default=1.0)
|
||||||
nPts The number of points in the returned lookup table.
|
nPts The number of points in the returned lookup table.
|
||||||
alpha True, False, or None - Specifies whether or not alpha values are included
|
alpha True, False, or None - Specifies whether or not alpha values are included
|
||||||
in the table. If alpha is None, it will be automatically determined.
|
in the table. If alpha is None, it will be automatically determined.
|
||||||
mode Determines return type: 'byte' (0-255), 'float' (0.0-1.0), or 'qcolor'.
|
mode Determines return type: 'byte' (0-255), 'float' (0.0-1.0), or 'qcolor'.
|
||||||
See :func:`map() <pyqtgraph.ColorMap.map>`.
|
See :func:`map() <pyqtgraph.ColorMap.map>`.
|
||||||
============= ============================================================================
|
=============== =============================================================================
|
||||||
"""
|
"""
|
||||||
if isinstance(mode, basestring):
|
if isinstance(mode, basestring):
|
||||||
mode = self.enumMap[mode.lower()]
|
mode = self.enumMap[mode.lower()]
|
||||||
|
@ -31,16 +31,16 @@ class ConsoleWidget(QtGui.QWidget):
|
|||||||
|
|
||||||
def __init__(self, parent=None, namespace=None, historyFile=None, text=None, editor=None):
|
def __init__(self, parent=None, namespace=None, historyFile=None, text=None, editor=None):
|
||||||
"""
|
"""
|
||||||
============ ============================================================================
|
============== ============================================================================
|
||||||
Arguments:
|
**Arguments:**
|
||||||
namespace dictionary containing the initial variables present in the default namespace
|
namespace dictionary containing the initial variables present in the default namespace
|
||||||
historyFile optional file for storing command history
|
historyFile optional file for storing command history
|
||||||
text initial text to display in the console window
|
text initial text to display in the console window
|
||||||
editor optional string for invoking code editor (called when stack trace entries are
|
editor optional string for invoking code editor (called when stack trace entries are
|
||||||
double-clicked). May contain {fileName} and {lineNum} format keys. Example::
|
double-clicked). May contain {fileName} and {lineNum} format keys. Example::
|
||||||
|
|
||||||
editorCommand --loadfile {fileName} --gotoline {lineNum}
|
editorCommand --loadfile {fileName} --gotoline {lineNum}
|
||||||
============ =============================================================================
|
============== =============================================================================
|
||||||
"""
|
"""
|
||||||
QtGui.QWidget.__init__(self, parent)
|
QtGui.QWidget.__init__(self, parent)
|
||||||
if namespace is None:
|
if namespace is None:
|
||||||
|
171
debug.py
171
debug.py
@ -7,10 +7,12 @@ Distributed under MIT/X11 license. See license.txt for more infomation.
|
|||||||
|
|
||||||
from __future__ import print_function
|
from __future__ import print_function
|
||||||
|
|
||||||
import sys, traceback, time, gc, re, types, weakref, inspect, os, cProfile
|
import sys, traceback, time, gc, re, types, weakref, inspect, os, cProfile, threading
|
||||||
from . import ptime
|
from . import ptime
|
||||||
from numpy import ndarray
|
from numpy import ndarray
|
||||||
from .Qt import QtCore, QtGui
|
from .Qt import QtCore, QtGui
|
||||||
|
from .util.mutex import Mutex
|
||||||
|
from .util import cprint
|
||||||
|
|
||||||
__ftraceDepth = 0
|
__ftraceDepth = 0
|
||||||
def ftrace(func):
|
def ftrace(func):
|
||||||
@ -238,7 +240,8 @@ def refPathString(chain):
|
|||||||
|
|
||||||
def objectSize(obj, ignore=None, verbose=False, depth=0, recursive=False):
|
def objectSize(obj, ignore=None, verbose=False, depth=0, recursive=False):
|
||||||
"""Guess how much memory an object is using"""
|
"""Guess how much memory an object is using"""
|
||||||
ignoreTypes = [types.MethodType, types.UnboundMethodType, types.BuiltinMethodType, types.FunctionType, types.BuiltinFunctionType]
|
ignoreTypes = ['MethodType', 'UnboundMethodType', 'BuiltinMethodType', 'FunctionType', 'BuiltinFunctionType']
|
||||||
|
ignoreTypes = [getattr(types, key) for key in ignoreTypes if hasattr(types, key)]
|
||||||
ignoreRegex = re.compile('(method-wrapper|Flag|ItemChange|Option|Mode)')
|
ignoreRegex = re.compile('(method-wrapper|Flag|ItemChange|Option|Mode)')
|
||||||
|
|
||||||
|
|
||||||
@ -399,7 +402,9 @@ class Profiler(object):
|
|||||||
only the initial "pyqtgraph." prefix from the module.
|
only the initial "pyqtgraph." prefix from the module.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
_profilers = os.environ.get("PYQTGRAPHPROFILE", "")
|
_profilers = os.environ.get("PYQTGRAPHPROFILE", None)
|
||||||
|
_profilers = _profilers.split(",") if _profilers is not None else []
|
||||||
|
|
||||||
_depth = 0
|
_depth = 0
|
||||||
_msgs = []
|
_msgs = []
|
||||||
|
|
||||||
@ -415,38 +420,36 @@ class Profiler(object):
|
|||||||
_disabledProfiler = DisabledProfiler()
|
_disabledProfiler = DisabledProfiler()
|
||||||
|
|
||||||
|
|
||||||
if _profilers:
|
def __new__(cls, msg=None, disabled='env', delayed=True):
|
||||||
_profilers = _profilers.split(",")
|
"""Optionally create a new profiler based on caller's qualname.
|
||||||
def __new__(cls, msg=None, disabled='env', delayed=True):
|
"""
|
||||||
"""Optionally create a new profiler based on caller's qualname.
|
if disabled is True or (disabled=='env' and len(cls._profilers) == 0):
|
||||||
"""
|
return cls._disabledProfiler
|
||||||
if disabled is True:
|
|
||||||
return cls._disabledProfiler
|
# determine the qualified name of the caller function
|
||||||
|
caller_frame = sys._getframe(1)
|
||||||
# determine the qualified name of the caller function
|
try:
|
||||||
caller_frame = sys._getframe(1)
|
caller_object_type = type(caller_frame.f_locals["self"])
|
||||||
try:
|
except KeyError: # we are in a regular function
|
||||||
caller_object_type = type(caller_frame.f_locals["self"])
|
qualifier = caller_frame.f_globals["__name__"].split(".", 1)[-1]
|
||||||
except KeyError: # we are in a regular function
|
else: # we are in a method
|
||||||
qualifier = caller_frame.f_globals["__name__"].split(".", 1)[1]
|
qualifier = caller_object_type.__name__
|
||||||
else: # we are in a method
|
func_qualname = qualifier + "." + caller_frame.f_code.co_name
|
||||||
qualifier = caller_object_type.__name__
|
if disabled=='env' and func_qualname not in cls._profilers: # don't do anything
|
||||||
func_qualname = qualifier + "." + caller_frame.f_code.co_name
|
return cls._disabledProfiler
|
||||||
if func_qualname not in cls._profilers: # don't do anything
|
# create an actual profiling object
|
||||||
return cls._disabledProfiler
|
cls._depth += 1
|
||||||
# create an actual profiling object
|
obj = super(Profiler, cls).__new__(cls)
|
||||||
cls._depth += 1
|
obj._name = msg or func_qualname
|
||||||
obj = super(Profiler, cls).__new__(cls)
|
obj._delayed = delayed
|
||||||
obj._name = msg or func_qualname
|
obj._markCount = 0
|
||||||
obj._delayed = delayed
|
obj._finished = False
|
||||||
obj._markCount = 0
|
obj._firstTime = obj._lastTime = ptime.time()
|
||||||
obj._finished = False
|
obj._newMsg("> Entering " + obj._name)
|
||||||
obj._firstTime = obj._lastTime = ptime.time()
|
return obj
|
||||||
obj._newMsg("> Entering " + obj._name)
|
#else:
|
||||||
return obj
|
#def __new__(cls, delayed=True):
|
||||||
else:
|
#return lambda msg=None: None
|
||||||
def __new__(cls, delayed=True):
|
|
||||||
return lambda msg=None: None
|
|
||||||
|
|
||||||
def __call__(self, msg=None):
|
def __call__(self, msg=None):
|
||||||
"""Register or print a new message with timing information.
|
"""Register or print a new message with timing information.
|
||||||
@ -467,6 +470,7 @@ class Profiler(object):
|
|||||||
if self._delayed:
|
if self._delayed:
|
||||||
self._msgs.append((msg, args))
|
self._msgs.append((msg, args))
|
||||||
else:
|
else:
|
||||||
|
self.flush()
|
||||||
print(msg % args)
|
print(msg % args)
|
||||||
|
|
||||||
def __del__(self):
|
def __del__(self):
|
||||||
@ -483,10 +487,13 @@ class Profiler(object):
|
|||||||
self._newMsg("< Exiting %s, total time: %0.4f ms",
|
self._newMsg("< Exiting %s, total time: %0.4f ms",
|
||||||
self._name, (ptime.time() - self._firstTime) * 1000)
|
self._name, (ptime.time() - self._firstTime) * 1000)
|
||||||
type(self)._depth -= 1
|
type(self)._depth -= 1
|
||||||
if self._depth < 1 and self._msgs:
|
if self._depth < 1:
|
||||||
|
self.flush()
|
||||||
|
|
||||||
|
def flush(self):
|
||||||
|
if self._msgs:
|
||||||
print("\n".join([m[0]%m[1] for m in self._msgs]))
|
print("\n".join([m[0]%m[1] for m in self._msgs]))
|
||||||
type(self)._msgs = []
|
type(self)._msgs = []
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def profile(code, name='profile_run', sort='cumulative', num=30):
|
def profile(code, name='profile_run', sort='cumulative', num=30):
|
||||||
@ -618,12 +625,12 @@ class ObjTracker(object):
|
|||||||
|
|
||||||
## Which refs have disappeared since call to start() (these are only displayed once, then forgotten.)
|
## Which refs have disappeared since call to start() (these are only displayed once, then forgotten.)
|
||||||
delRefs = {}
|
delRefs = {}
|
||||||
for i in self.startRefs.keys():
|
for i in list(self.startRefs.keys()):
|
||||||
if i not in refs:
|
if i not in refs:
|
||||||
delRefs[i] = self.startRefs[i]
|
delRefs[i] = self.startRefs[i]
|
||||||
del self.startRefs[i]
|
del self.startRefs[i]
|
||||||
self.forgetRef(delRefs[i])
|
self.forgetRef(delRefs[i])
|
||||||
for i in self.newRefs.keys():
|
for i in list(self.newRefs.keys()):
|
||||||
if i not in refs:
|
if i not in refs:
|
||||||
delRefs[i] = self.newRefs[i]
|
delRefs[i] = self.newRefs[i]
|
||||||
del self.newRefs[i]
|
del self.newRefs[i]
|
||||||
@ -661,7 +668,8 @@ class ObjTracker(object):
|
|||||||
for k in self.startCount:
|
for k in self.startCount:
|
||||||
c1[k] = c1.get(k, 0) - self.startCount[k]
|
c1[k] = c1.get(k, 0) - self.startCount[k]
|
||||||
typs = list(c1.keys())
|
typs = list(c1.keys())
|
||||||
typs.sort(lambda a,b: cmp(c1[a], c1[b]))
|
#typs.sort(lambda a,b: cmp(c1[a], c1[b]))
|
||||||
|
typs.sort(key=lambda a: c1[a])
|
||||||
for t in typs:
|
for t in typs:
|
||||||
if c1[t] == 0:
|
if c1[t] == 0:
|
||||||
continue
|
continue
|
||||||
@ -761,7 +769,8 @@ class ObjTracker(object):
|
|||||||
c = count.get(typ, [0,0])
|
c = count.get(typ, [0,0])
|
||||||
count[typ] = [c[0]+1, c[1]+objectSize(obj)]
|
count[typ] = [c[0]+1, c[1]+objectSize(obj)]
|
||||||
typs = list(count.keys())
|
typs = list(count.keys())
|
||||||
typs.sort(lambda a,b: cmp(count[a][1], count[b][1]))
|
#typs.sort(lambda a,b: cmp(count[a][1], count[b][1]))
|
||||||
|
typs.sort(key=lambda a: count[a][1])
|
||||||
|
|
||||||
for t in typs:
|
for t in typs:
|
||||||
line = " %d\t%d\t%s" % (count[t][0], count[t][1], t)
|
line = " %d\t%d\t%s" % (count[t][0], count[t][1], t)
|
||||||
@ -821,14 +830,15 @@ def describeObj(obj, depth=4, path=None, ignore=None):
|
|||||||
def typeStr(obj):
|
def typeStr(obj):
|
||||||
"""Create a more useful type string by making <instance> types report their class."""
|
"""Create a more useful type string by making <instance> types report their class."""
|
||||||
typ = type(obj)
|
typ = type(obj)
|
||||||
if typ == types.InstanceType:
|
if typ == getattr(types, 'InstanceType', None):
|
||||||
return "<instance of %s>" % obj.__class__.__name__
|
return "<instance of %s>" % obj.__class__.__name__
|
||||||
else:
|
else:
|
||||||
return str(typ)
|
return str(typ)
|
||||||
|
|
||||||
def searchRefs(obj, *args):
|
def searchRefs(obj, *args):
|
||||||
"""Pseudo-interactive function for tracing references backward.
|
"""Pseudo-interactive function for tracing references backward.
|
||||||
Arguments:
|
**Arguments:**
|
||||||
|
|
||||||
obj: The initial object from which to start searching
|
obj: The initial object from which to start searching
|
||||||
args: A set of string or int arguments.
|
args: A set of string or int arguments.
|
||||||
each integer selects one of obj's referrers to be the new 'obj'
|
each integer selects one of obj's referrers to be the new 'obj'
|
||||||
@ -840,7 +850,8 @@ def searchRefs(obj, *args):
|
|||||||
ro: return obj
|
ro: return obj
|
||||||
rr: return list of obj's referrers
|
rr: return list of obj's referrers
|
||||||
|
|
||||||
Examples:
|
Examples::
|
||||||
|
|
||||||
searchRefs(obj, 't') ## Print types of all objects referring to obj
|
searchRefs(obj, 't') ## Print types of all objects referring to obj
|
||||||
searchRefs(obj, 't', 0, 't') ## ..then select the first referrer and print the types of its referrers
|
searchRefs(obj, 't', 0, 't') ## ..then select the first referrer and print the types of its referrers
|
||||||
searchRefs(obj, 't', 0, 't', 'l') ## ..also print lengths of the last set of referrers
|
searchRefs(obj, 't', 0, 't', 'l') ## ..also print lengths of the last set of referrers
|
||||||
@ -989,3 +1000,75 @@ class PrintDetector(object):
|
|||||||
|
|
||||||
def flush(self):
|
def flush(self):
|
||||||
self.stdout.flush()
|
self.stdout.flush()
|
||||||
|
|
||||||
|
|
||||||
|
class PeriodicTrace(object):
|
||||||
|
"""
|
||||||
|
Used to debug freezing by starting a new thread that reports on the
|
||||||
|
location of the main thread periodically.
|
||||||
|
"""
|
||||||
|
class ReportThread(QtCore.QThread):
|
||||||
|
def __init__(self):
|
||||||
|
self.frame = None
|
||||||
|
self.ind = 0
|
||||||
|
self.lastInd = None
|
||||||
|
self.lock = Mutex()
|
||||||
|
QtCore.QThread.__init__(self)
|
||||||
|
|
||||||
|
def notify(self, frame):
|
||||||
|
with self.lock:
|
||||||
|
self.frame = frame
|
||||||
|
self.ind += 1
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
while True:
|
||||||
|
time.sleep(1)
|
||||||
|
with self.lock:
|
||||||
|
if self.lastInd != self.ind:
|
||||||
|
print("== Trace %d: ==" % self.ind)
|
||||||
|
traceback.print_stack(self.frame)
|
||||||
|
self.lastInd = self.ind
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.mainThread = threading.current_thread()
|
||||||
|
self.thread = PeriodicTrace.ReportThread()
|
||||||
|
self.thread.start()
|
||||||
|
sys.settrace(self.trace)
|
||||||
|
|
||||||
|
def trace(self, frame, event, arg):
|
||||||
|
if threading.current_thread() is self.mainThread: # and 'threading' not in frame.f_code.co_filename:
|
||||||
|
self.thread.notify(frame)
|
||||||
|
# print("== Trace ==", event, arg)
|
||||||
|
# traceback.print_stack(frame)
|
||||||
|
return self.trace
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class ThreadColor(object):
|
||||||
|
"""
|
||||||
|
Wrapper on stdout/stderr that colors text by the current thread ID.
|
||||||
|
|
||||||
|
*stream* must be 'stdout' or 'stderr'.
|
||||||
|
"""
|
||||||
|
colors = {}
|
||||||
|
lock = Mutex()
|
||||||
|
|
||||||
|
def __init__(self, stream):
|
||||||
|
self.stream = getattr(sys, stream)
|
||||||
|
self.err = stream == 'stderr'
|
||||||
|
setattr(sys, stream, self)
|
||||||
|
|
||||||
|
def write(self, msg):
|
||||||
|
with self.lock:
|
||||||
|
cprint.cprint(self.stream, self.color(), msg, -1, stderr=self.err)
|
||||||
|
|
||||||
|
def flush(self):
|
||||||
|
with self.lock:
|
||||||
|
self.stream.flush()
|
||||||
|
|
||||||
|
def color(self):
|
||||||
|
tid = threading.current_thread()
|
||||||
|
if tid not in self.colors:
|
||||||
|
c = (len(self.colors) % 15) + 1
|
||||||
|
self.colors[tid] = c
|
||||||
|
return self.colors[tid]
|
||||||
|
@ -2,6 +2,7 @@ from ..Qt import QtCore, QtGui
|
|||||||
|
|
||||||
from .DockDrop import *
|
from .DockDrop import *
|
||||||
from ..widgets.VerticalLabel import VerticalLabel
|
from ..widgets.VerticalLabel import VerticalLabel
|
||||||
|
from ..python2_3 import asUnicode
|
||||||
|
|
||||||
class Dock(QtGui.QWidget, DockDrop):
|
class Dock(QtGui.QWidget, DockDrop):
|
||||||
|
|
||||||
@ -167,7 +168,7 @@ class Dock(QtGui.QWidget, DockDrop):
|
|||||||
self.resizeOverlay(self.size())
|
self.resizeOverlay(self.size())
|
||||||
|
|
||||||
def name(self):
|
def name(self):
|
||||||
return str(self.label.text())
|
return asUnicode(self.label.text())
|
||||||
|
|
||||||
def container(self):
|
def container(self):
|
||||||
return self._container
|
return self._container
|
||||||
|
@ -36,16 +36,16 @@ class DockArea(Container, QtGui.QWidget, DockDrop):
|
|||||||
def addDock(self, dock=None, position='bottom', relativeTo=None, **kwds):
|
def addDock(self, dock=None, position='bottom', relativeTo=None, **kwds):
|
||||||
"""Adds a dock to this area.
|
"""Adds a dock to this area.
|
||||||
|
|
||||||
=========== =================================================================
|
============== =================================================================
|
||||||
Arguments:
|
**Arguments:**
|
||||||
dock The new Dock object to add. If None, then a new Dock will be
|
dock The new Dock object to add. If None, then a new Dock will be
|
||||||
created.
|
created.
|
||||||
position 'bottom', 'top', 'left', 'right', 'above', or 'below'
|
position 'bottom', 'top', 'left', 'right', 'above', or 'below'
|
||||||
relativeTo If relativeTo is None, then the new Dock is added to fill an
|
relativeTo If relativeTo is None, then the new Dock is added to fill an
|
||||||
entire edge of the window. If relativeTo is another Dock, then
|
entire edge of the window. If relativeTo is another Dock, then
|
||||||
the new Dock is placed adjacent to it (or in a tabbed
|
the new Dock is placed adjacent to it (or in a tabbed
|
||||||
configuration for 'above' and 'below').
|
configuration for 'above' and 'below').
|
||||||
=========== =================================================================
|
============== =================================================================
|
||||||
|
|
||||||
All extra keyword arguments are passed to Dock.__init__() if *dock* is
|
All extra keyword arguments are passed to Dock.__init__() if *dock* is
|
||||||
None.
|
None.
|
||||||
|
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 = QtGui.QPrintDialog(printer)
|
||||||
dialog.setWindowTitle("Print Document")
|
dialog.setWindowTitle("Print Document")
|
||||||
if dialog.exec_() != QtGui.QDialog.Accepted:
|
if dialog.exec_() != QtGui.QDialog.Accepted:
|
||||||
return;
|
return
|
||||||
|
|
||||||
#dpi = QtGui.QDesktopWidget().physicalDpiX()
|
#dpi = QtGui.QDesktopWidget().physicalDpiX()
|
||||||
|
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
from .Exporter import Exporter
|
from .Exporter import Exporter
|
||||||
from ..python2_3 import asUnicode
|
from ..python2_3 import asUnicode
|
||||||
from ..parametertree import Parameter
|
from ..parametertree import Parameter
|
||||||
from ..Qt import QtGui, QtCore, QtSvg
|
from ..Qt import QtGui, QtCore, QtSvg, USE_PYSIDE
|
||||||
from .. import debug
|
from .. import debug
|
||||||
from .. import functions as fn
|
from .. import functions as fn
|
||||||
import re
|
import re
|
||||||
@ -219,7 +219,10 @@ def _generateItemSvg(item, nodes=None, root=None):
|
|||||||
#if hasattr(item, 'setExportMode'):
|
#if hasattr(item, 'setExportMode'):
|
||||||
#item.setExportMode(False)
|
#item.setExportMode(False)
|
||||||
|
|
||||||
xmlStr = bytes(arr).decode('utf-8')
|
if USE_PYSIDE:
|
||||||
|
xmlStr = str(arr)
|
||||||
|
else:
|
||||||
|
xmlStr = bytes(arr).decode('utf-8')
|
||||||
doc = xml.parseString(xmlStr)
|
doc = xml.parseString(xmlStr)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
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,18 +227,11 @@ class Flowchart(Node):
|
|||||||
def nodeClosed(self, node):
|
def nodeClosed(self, node):
|
||||||
del self._nodes[node.name()]
|
del self._nodes[node.name()]
|
||||||
self.widget().removeNode(node)
|
self.widget().removeNode(node)
|
||||||
try:
|
for signal in ['sigClosed', 'sigRenamed', 'sigOutputChanged']:
|
||||||
node.sigClosed.disconnect(self.nodeClosed)
|
try:
|
||||||
except TypeError:
|
getattr(node, signal).disconnect(self.nodeClosed)
|
||||||
pass
|
except (TypeError, RuntimeError):
|
||||||
try:
|
pass
|
||||||
node.sigRenamed.disconnect(self.nodeRenamed)
|
|
||||||
except TypeError:
|
|
||||||
pass
|
|
||||||
try:
|
|
||||||
node.sigOutputChanged.disconnect(self.nodeOutputChanged)
|
|
||||||
except TypeError:
|
|
||||||
pass
|
|
||||||
self.sigChartChanged.emit(self, 'remove', node)
|
self.sigChartChanged.emit(self, 'remove', node)
|
||||||
|
|
||||||
def nodeRenamed(self, node, oldName):
|
def nodeRenamed(self, node, oldName):
|
||||||
@ -769,7 +762,7 @@ class FlowchartCtrlWidget(QtGui.QWidget):
|
|||||||
#self.disconnect(item.bypassBtn, QtCore.SIGNAL('clicked()'), self.bypassClicked)
|
#self.disconnect(item.bypassBtn, QtCore.SIGNAL('clicked()'), self.bypassClicked)
|
||||||
try:
|
try:
|
||||||
item.bypassBtn.clicked.disconnect(self.bypassClicked)
|
item.bypassBtn.clicked.disconnect(self.bypassClicked)
|
||||||
except TypeError:
|
except (TypeError, RuntimeError):
|
||||||
pass
|
pass
|
||||||
self.ui.ctrlList.removeTopLevelItem(item)
|
self.ui.ctrlList.removeTopLevelItem(item)
|
||||||
|
|
||||||
|
@ -37,7 +37,7 @@ class Node(QtCore.QObject):
|
|||||||
def __init__(self, name, terminals=None, allowAddInput=False, allowAddOutput=False, allowRemove=True):
|
def __init__(self, name, terminals=None, allowAddInput=False, allowAddOutput=False, allowRemove=True):
|
||||||
"""
|
"""
|
||||||
============== ============================================================
|
============== ============================================================
|
||||||
Arguments
|
**Arguments:**
|
||||||
name The name of this specific node instance. It can be any
|
name The name of this specific node instance. It can be any
|
||||||
string, but must be unique within a flowchart. Usually,
|
string, but must be unique within a flowchart. Usually,
|
||||||
we simply let the flowchart decide on a name when calling
|
we simply let the flowchart decide on a name when calling
|
||||||
@ -501,8 +501,8 @@ class NodeGraphicsItem(GraphicsObject):
|
|||||||
bounds = self.boundingRect()
|
bounds = self.boundingRect()
|
||||||
self.nameItem.setPos(bounds.width()/2. - self.nameItem.boundingRect().width()/2., 0)
|
self.nameItem.setPos(bounds.width()/2. - self.nameItem.boundingRect().width()/2., 0)
|
||||||
|
|
||||||
def setPen(self, pen):
|
def setPen(self, *args, **kwargs):
|
||||||
self.pen = pen
|
self.pen = fn.mkPen(*args, **kwargs)
|
||||||
self.update()
|
self.update()
|
||||||
|
|
||||||
def setBrush(self, brush):
|
def setBrush(self, brush):
|
||||||
|
@ -26,12 +26,14 @@ class NodeLibrary:
|
|||||||
Register a new node type. If the type's name is already in use,
|
Register a new node type. If the type's name is already in use,
|
||||||
an exception will be raised (unless override=True).
|
an exception will be raised (unless override=True).
|
||||||
|
|
||||||
Arguments:
|
============== =========================================================
|
||||||
|
**Arguments:**
|
||||||
|
|
||||||
nodeClass - a subclass of Node (must have typ.nodeName)
|
nodeClass a subclass of Node (must have typ.nodeName)
|
||||||
paths - list of tuples specifying the location(s) this
|
paths list of tuples specifying the location(s) this
|
||||||
type will appear in the library tree.
|
type will appear in the library tree.
|
||||||
override - if True, overwrite any class having the same name
|
override if True, overwrite any class having the same name
|
||||||
|
============== =========================================================
|
||||||
"""
|
"""
|
||||||
if not isNodeClass(nodeClass):
|
if not isNodeClass(nodeClass):
|
||||||
raise Exception("Object %s is not a Node subclass" % str(nodeClass))
|
raise Exception("Object %s is not a Node subclass" % str(nodeClass))
|
||||||
|
@ -29,7 +29,7 @@ def eq(a, b):
|
|||||||
except:
|
except:
|
||||||
return False
|
return False
|
||||||
if (hasattr(e, 'implements') and e.implements('MetaArray')):
|
if (hasattr(e, 'implements') and e.implements('MetaArray')):
|
||||||
return e.asarray().all()
|
return e.asarray().all()
|
||||||
else:
|
else:
|
||||||
return e.all()
|
return e.all()
|
||||||
else:
|
else:
|
||||||
|
@ -328,7 +328,7 @@ class ColumnJoinNode(Node):
|
|||||||
|
|
||||||
## Node.restoreState should have created all of the terminals we need
|
## Node.restoreState should have created all of the terminals we need
|
||||||
## However: to maintain support for some older flowchart files, we need
|
## However: to maintain support for some older flowchart files, we need
|
||||||
## to manually add any terminals that were not taken care of.
|
## to manually add any terminals that were not taken care of.
|
||||||
for name in [n for n in state['order'] if n not in inputs]:
|
for name in [n for n in state['order'] if n not in inputs]:
|
||||||
Node.addInput(self, name, renamable=True, removable=True, multiable=True)
|
Node.addInput(self, name, renamable=True, removable=True, multiable=True)
|
||||||
inputs = self.inputs()
|
inputs = self.inputs()
|
||||||
|
@ -1,10 +1,8 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
from ...Qt import QtCore, QtGui
|
from ...Qt import QtCore, QtGui
|
||||||
from ..Node import Node
|
from ..Node import Node
|
||||||
from scipy.signal import detrend
|
|
||||||
from scipy.ndimage import median_filter, gaussian_filter
|
|
||||||
#from ...SignalProxy import SignalProxy
|
|
||||||
from . import functions
|
from . import functions
|
||||||
|
from ... import functions as pgfn
|
||||||
from .common import *
|
from .common import *
|
||||||
import numpy as np
|
import numpy as np
|
||||||
|
|
||||||
@ -119,7 +117,11 @@ class Median(CtrlNode):
|
|||||||
|
|
||||||
@metaArrayWrapper
|
@metaArrayWrapper
|
||||||
def processData(self, data):
|
def processData(self, data):
|
||||||
return median_filter(data, self.ctrls['n'].value())
|
try:
|
||||||
|
import scipy.ndimage
|
||||||
|
except ImportError:
|
||||||
|
raise Exception("MedianFilter node requires the package scipy.ndimage.")
|
||||||
|
return scipy.ndimage.median_filter(data, self.ctrls['n'].value())
|
||||||
|
|
||||||
class Mode(CtrlNode):
|
class Mode(CtrlNode):
|
||||||
"""Filters data by taking the mode (histogram-based) of a sliding window"""
|
"""Filters data by taking the mode (histogram-based) of a sliding window"""
|
||||||
@ -156,7 +158,11 @@ class Gaussian(CtrlNode):
|
|||||||
|
|
||||||
@metaArrayWrapper
|
@metaArrayWrapper
|
||||||
def processData(self, data):
|
def processData(self, data):
|
||||||
return gaussian_filter(data, self.ctrls['sigma'].value())
|
try:
|
||||||
|
import scipy.ndimage
|
||||||
|
except ImportError:
|
||||||
|
raise Exception("GaussianFilter node requires the package scipy.ndimage.")
|
||||||
|
return pgfn.gaussianFilter(data, self.ctrls['sigma'].value())
|
||||||
|
|
||||||
|
|
||||||
class Derivative(CtrlNode):
|
class Derivative(CtrlNode):
|
||||||
@ -189,6 +195,10 @@ class Detrend(CtrlNode):
|
|||||||
|
|
||||||
@metaArrayWrapper
|
@metaArrayWrapper
|
||||||
def processData(self, data):
|
def processData(self, data):
|
||||||
|
try:
|
||||||
|
from scipy.signal import detrend
|
||||||
|
except ImportError:
|
||||||
|
raise Exception("DetrendFilter node requires the package scipy.signal.")
|
||||||
return detrend(data)
|
return detrend(data)
|
||||||
|
|
||||||
|
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
import scipy
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
from ...metaarray import MetaArray
|
from ...metaarray import MetaArray
|
||||||
|
|
||||||
@ -47,6 +46,11 @@ def downsample(data, n, axis=0, xvals='subsample'):
|
|||||||
def applyFilter(data, b, a, padding=100, bidir=True):
|
def applyFilter(data, b, a, padding=100, bidir=True):
|
||||||
"""Apply a linear filter with coefficients a, b. Optionally pad the data before filtering
|
"""Apply a linear filter with coefficients a, b. Optionally pad the data before filtering
|
||||||
and/or run the filter in both directions."""
|
and/or run the filter in both directions."""
|
||||||
|
try:
|
||||||
|
import scipy.signal
|
||||||
|
except ImportError:
|
||||||
|
raise Exception("applyFilter() requires the package scipy.signal.")
|
||||||
|
|
||||||
d1 = data.view(np.ndarray)
|
d1 = data.view(np.ndarray)
|
||||||
|
|
||||||
if padding > 0:
|
if padding > 0:
|
||||||
@ -67,6 +71,11 @@ def applyFilter(data, b, a, padding=100, bidir=True):
|
|||||||
|
|
||||||
def besselFilter(data, cutoff, order=1, dt=None, btype='low', bidir=True):
|
def besselFilter(data, cutoff, order=1, dt=None, btype='low', bidir=True):
|
||||||
"""return data passed through bessel filter"""
|
"""return data passed through bessel filter"""
|
||||||
|
try:
|
||||||
|
import scipy.signal
|
||||||
|
except ImportError:
|
||||||
|
raise Exception("besselFilter() requires the package scipy.signal.")
|
||||||
|
|
||||||
if dt is None:
|
if dt is None:
|
||||||
try:
|
try:
|
||||||
tvals = data.xvals('Time')
|
tvals = data.xvals('Time')
|
||||||
@ -85,6 +94,11 @@ def besselFilter(data, cutoff, order=1, dt=None, btype='low', bidir=True):
|
|||||||
|
|
||||||
def butterworthFilter(data, wPass, wStop=None, gPass=2.0, gStop=20.0, order=1, dt=None, btype='low', bidir=True):
|
def butterworthFilter(data, wPass, wStop=None, gPass=2.0, gStop=20.0, order=1, dt=None, btype='low', bidir=True):
|
||||||
"""return data passed through bessel filter"""
|
"""return data passed through bessel filter"""
|
||||||
|
try:
|
||||||
|
import scipy.signal
|
||||||
|
except ImportError:
|
||||||
|
raise Exception("butterworthFilter() requires the package scipy.signal.")
|
||||||
|
|
||||||
if dt is None:
|
if dt is None:
|
||||||
try:
|
try:
|
||||||
tvals = data.xvals('Time')
|
tvals = data.xvals('Time')
|
||||||
@ -175,6 +189,11 @@ def denoise(data, radius=2, threshold=4):
|
|||||||
|
|
||||||
def adaptiveDetrend(data, x=None, threshold=3.0):
|
def adaptiveDetrend(data, x=None, threshold=3.0):
|
||||||
"""Return the signal with baseline removed. Discards outliers from baseline measurement."""
|
"""Return the signal with baseline removed. Discards outliers from baseline measurement."""
|
||||||
|
try:
|
||||||
|
import scipy.signal
|
||||||
|
except ImportError:
|
||||||
|
raise Exception("adaptiveDetrend() requires the package scipy.signal.")
|
||||||
|
|
||||||
if x is None:
|
if x is None:
|
||||||
x = data.xvals(0)
|
x = data.xvals(0)
|
||||||
|
|
||||||
|
102
frozenSupport.py
102
frozenSupport.py
@ -1,52 +1,52 @@
|
|||||||
## Definitions helpful in frozen environments (eg py2exe)
|
## Definitions helpful in frozen environments (eg py2exe)
|
||||||
import os, sys, zipfile
|
import os, sys, zipfile
|
||||||
|
|
||||||
def listdir(path):
|
def listdir(path):
|
||||||
"""Replacement for os.listdir that works in frozen environments."""
|
"""Replacement for os.listdir that works in frozen environments."""
|
||||||
if not hasattr(sys, 'frozen'):
|
if not hasattr(sys, 'frozen'):
|
||||||
return os.listdir(path)
|
return os.listdir(path)
|
||||||
|
|
||||||
(zipPath, archivePath) = splitZip(path)
|
(zipPath, archivePath) = splitZip(path)
|
||||||
if archivePath is None:
|
if archivePath is None:
|
||||||
return os.listdir(path)
|
return os.listdir(path)
|
||||||
|
|
||||||
with zipfile.ZipFile(zipPath, "r") as zipobj:
|
with zipfile.ZipFile(zipPath, "r") as zipobj:
|
||||||
contents = zipobj.namelist()
|
contents = zipobj.namelist()
|
||||||
results = set()
|
results = set()
|
||||||
for name in contents:
|
for name in contents:
|
||||||
# components in zip archive paths are always separated by forward slash
|
# components in zip archive paths are always separated by forward slash
|
||||||
if name.startswith(archivePath) and len(name) > len(archivePath):
|
if name.startswith(archivePath) and len(name) > len(archivePath):
|
||||||
name = name[len(archivePath):].split('/')[0]
|
name = name[len(archivePath):].split('/')[0]
|
||||||
results.add(name)
|
results.add(name)
|
||||||
return list(results)
|
return list(results)
|
||||||
|
|
||||||
def isdir(path):
|
def isdir(path):
|
||||||
"""Replacement for os.path.isdir that works in frozen environments."""
|
"""Replacement for os.path.isdir that works in frozen environments."""
|
||||||
if not hasattr(sys, 'frozen'):
|
if not hasattr(sys, 'frozen'):
|
||||||
return os.path.isdir(path)
|
return os.path.isdir(path)
|
||||||
|
|
||||||
(zipPath, archivePath) = splitZip(path)
|
(zipPath, archivePath) = splitZip(path)
|
||||||
if archivePath is None:
|
if archivePath is None:
|
||||||
return os.path.isdir(path)
|
return os.path.isdir(path)
|
||||||
with zipfile.ZipFile(zipPath, "r") as zipobj:
|
with zipfile.ZipFile(zipPath, "r") as zipobj:
|
||||||
contents = zipobj.namelist()
|
contents = zipobj.namelist()
|
||||||
archivePath = archivePath.rstrip('/') + '/' ## make sure there's exactly one '/' at the end
|
archivePath = archivePath.rstrip('/') + '/' ## make sure there's exactly one '/' at the end
|
||||||
for c in contents:
|
for c in contents:
|
||||||
if c.startswith(archivePath):
|
if c.startswith(archivePath):
|
||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
def splitZip(path):
|
def splitZip(path):
|
||||||
"""Splits a path containing a zip file into (zipfile, subpath).
|
"""Splits a path containing a zip file into (zipfile, subpath).
|
||||||
If there is no zip file, returns (path, None)"""
|
If there is no zip file, returns (path, None)"""
|
||||||
components = os.path.normpath(path).split(os.sep)
|
components = os.path.normpath(path).split(os.sep)
|
||||||
for index, component in enumerate(components):
|
for index, component in enumerate(components):
|
||||||
if component.endswith('.zip'):
|
if component.endswith('.zip'):
|
||||||
zipPath = os.sep.join(components[0:index+1])
|
zipPath = os.sep.join(components[0:index+1])
|
||||||
archivePath = ''.join([x+'/' for x in components[index+1:]])
|
archivePath = ''.join([x+'/' for x in components[index+1:]])
|
||||||
return (zipPath, archivePath)
|
return (zipPath, archivePath)
|
||||||
else:
|
else:
|
||||||
return (path, None)
|
return (path, None)
|
||||||
|
|
||||||
|
|
574
functions.py
574
functions.py
@ -34,17 +34,6 @@ import decimal, re
|
|||||||
import ctypes
|
import ctypes
|
||||||
import sys, struct
|
import sys, struct
|
||||||
|
|
||||||
try:
|
|
||||||
import scipy.ndimage
|
|
||||||
HAVE_SCIPY = True
|
|
||||||
if getConfigOption('useWeave'):
|
|
||||||
try:
|
|
||||||
import scipy.weave
|
|
||||||
except ImportError:
|
|
||||||
setConfigOptions(useWeave=False)
|
|
||||||
except ImportError:
|
|
||||||
HAVE_SCIPY = False
|
|
||||||
|
|
||||||
from . import debug
|
from . import debug
|
||||||
|
|
||||||
def siScale(x, minVal=1e-25, allowUnicode=True):
|
def siScale(x, minVal=1e-25, allowUnicode=True):
|
||||||
@ -383,12 +372,12 @@ def affineSlice(data, shape, origin, vectors, axes, order=1, returnCoords=False,
|
|||||||
"""
|
"""
|
||||||
Take a slice of any orientation through an array. This is useful for extracting sections of multi-dimensional arrays such as MRI images for viewing as 1D or 2D data.
|
Take a slice of any orientation through an array. This is useful for extracting sections of multi-dimensional arrays such as MRI images for viewing as 1D or 2D data.
|
||||||
|
|
||||||
The slicing axes are aribtrary; they do not need to be orthogonal to the original data or even to each other. It is possible to use this function to extract arbitrary linear, rectangular, or parallelepiped shapes from within larger datasets. The original data is interpolated onto a new array of coordinates using scipy.ndimage.map_coordinates (see the scipy documentation for more information about this).
|
The slicing axes are aribtrary; they do not need to be orthogonal to the original data or even to each other. It is possible to use this function to extract arbitrary linear, rectangular, or parallelepiped shapes from within larger datasets. The original data is interpolated onto a new array of coordinates using scipy.ndimage.map_coordinates if it is available (see the scipy documentation for more information about this). If scipy is not available, then a slower implementation of map_coordinates is used.
|
||||||
|
|
||||||
For a graphical interface to this function, see :func:`ROI.getArrayRegion <pyqtgraph.ROI.getArrayRegion>`
|
For a graphical interface to this function, see :func:`ROI.getArrayRegion <pyqtgraph.ROI.getArrayRegion>`
|
||||||
|
|
||||||
============== ====================================================================================================
|
============== ====================================================================================================
|
||||||
Arguments:
|
**Arguments:**
|
||||||
*data* (ndarray) the original dataset
|
*data* (ndarray) the original dataset
|
||||||
*shape* the shape of the slice to take (Note the return value may have more dimensions than len(shape))
|
*shape* the shape of the slice to take (Note the return value may have more dimensions than len(shape))
|
||||||
*origin* the location in the original dataset that will become the origin of the sliced data.
|
*origin* the location in the original dataset that will become the origin of the sliced data.
|
||||||
@ -422,8 +411,12 @@ def affineSlice(data, shape, origin, vectors, axes, order=1, returnCoords=False,
|
|||||||
affineSlice(data, shape=(20,20), origin=(40,0,0), vectors=((-1, 1, 0), (-1, 0, 1)), axes=(1,2,3))
|
affineSlice(data, shape=(20,20), origin=(40,0,0), vectors=((-1, 1, 0), (-1, 0, 1)), axes=(1,2,3))
|
||||||
|
|
||||||
"""
|
"""
|
||||||
if not HAVE_SCIPY:
|
try:
|
||||||
raise Exception("This function requires the scipy library, but it does not appear to be importable.")
|
import scipy.ndimage
|
||||||
|
have_scipy = True
|
||||||
|
except ImportError:
|
||||||
|
have_scipy = False
|
||||||
|
have_scipy = False
|
||||||
|
|
||||||
# sanity check
|
# sanity check
|
||||||
if len(shape) != len(vectors):
|
if len(shape) != len(vectors):
|
||||||
@ -445,7 +438,6 @@ def affineSlice(data, shape, origin, vectors, axes, order=1, returnCoords=False,
|
|||||||
#print "tr1:", tr1
|
#print "tr1:", tr1
|
||||||
## dims are now [(slice axes), (other axes)]
|
## dims are now [(slice axes), (other axes)]
|
||||||
|
|
||||||
|
|
||||||
## make sure vectors are arrays
|
## make sure vectors are arrays
|
||||||
if not isinstance(vectors, np.ndarray):
|
if not isinstance(vectors, np.ndarray):
|
||||||
vectors = np.array(vectors)
|
vectors = np.array(vectors)
|
||||||
@ -461,12 +453,18 @@ def affineSlice(data, shape, origin, vectors, axes, order=1, returnCoords=False,
|
|||||||
#print "X values:"
|
#print "X values:"
|
||||||
#print x
|
#print x
|
||||||
## iterate manually over unused axes since map_coordinates won't do it for us
|
## iterate manually over unused axes since map_coordinates won't do it for us
|
||||||
extraShape = data.shape[len(axes):]
|
if have_scipy:
|
||||||
output = np.empty(tuple(shape) + extraShape, dtype=data.dtype)
|
extraShape = data.shape[len(axes):]
|
||||||
for inds in np.ndindex(*extraShape):
|
output = np.empty(tuple(shape) + extraShape, dtype=data.dtype)
|
||||||
ind = (Ellipsis,) + inds
|
for inds in np.ndindex(*extraShape):
|
||||||
#print data[ind].shape, x.shape, output[ind].shape, output.shape
|
ind = (Ellipsis,) + inds
|
||||||
output[ind] = scipy.ndimage.map_coordinates(data[ind], x, order=order, **kargs)
|
output[ind] = scipy.ndimage.map_coordinates(data[ind], x, order=order, **kargs)
|
||||||
|
else:
|
||||||
|
# map_coordinates expects the indexes as the first axis, whereas
|
||||||
|
# interpolateArray expects indexes at the last axis.
|
||||||
|
tr = tuple(range(1,x.ndim)) + (0,)
|
||||||
|
output = interpolateArray(data, x.transpose(tr))
|
||||||
|
|
||||||
|
|
||||||
tr = list(range(output.ndim))
|
tr = list(range(output.ndim))
|
||||||
trb = []
|
trb = []
|
||||||
@ -483,6 +481,117 @@ def affineSlice(data, shape, origin, vectors, axes, order=1, returnCoords=False,
|
|||||||
else:
|
else:
|
||||||
return output
|
return output
|
||||||
|
|
||||||
|
def interpolateArray(data, x, default=0.0):
|
||||||
|
"""
|
||||||
|
N-dimensional interpolation similar scipy.ndimage.map_coordinates.
|
||||||
|
|
||||||
|
This function returns linearly-interpolated values sampled from a regular
|
||||||
|
grid of data.
|
||||||
|
|
||||||
|
*data* is an array of any shape containing the values to be interpolated.
|
||||||
|
*x* is an array with (shape[-1] <= data.ndim) containing the locations
|
||||||
|
within *data* to interpolate.
|
||||||
|
|
||||||
|
Returns array of shape (x.shape[:-1] + data.shape)
|
||||||
|
|
||||||
|
For example, assume we have the following 2D image data::
|
||||||
|
|
||||||
|
>>> data = np.array([[1, 2, 4 ],
|
||||||
|
[10, 20, 40 ],
|
||||||
|
[100, 200, 400]])
|
||||||
|
|
||||||
|
To compute a single interpolated point from this data::
|
||||||
|
|
||||||
|
>>> x = np.array([(0.5, 0.5)])
|
||||||
|
>>> interpolateArray(data, x)
|
||||||
|
array([ 8.25])
|
||||||
|
|
||||||
|
To compute a 1D list of interpolated locations::
|
||||||
|
|
||||||
|
>>> x = np.array([(0.5, 0.5),
|
||||||
|
(1.0, 1.0),
|
||||||
|
(1.0, 2.0),
|
||||||
|
(1.5, 0.0)])
|
||||||
|
>>> interpolateArray(data, x)
|
||||||
|
array([ 8.25, 20. , 40. , 55. ])
|
||||||
|
|
||||||
|
To compute a 2D array of interpolated locations::
|
||||||
|
|
||||||
|
>>> x = np.array([[(0.5, 0.5), (1.0, 2.0)],
|
||||||
|
[(1.0, 1.0), (1.5, 0.0)]])
|
||||||
|
>>> interpolateArray(data, x)
|
||||||
|
array([[ 8.25, 40. ],
|
||||||
|
[ 20. , 55. ]])
|
||||||
|
|
||||||
|
..and so on. The *x* argument may have any shape as long as
|
||||||
|
```x.shape[-1] <= data.ndim```. In the case that
|
||||||
|
```x.shape[-1] < data.ndim```, then the remaining axes are simply
|
||||||
|
broadcasted as usual. For example, we can interpolate one location
|
||||||
|
from an entire row of the data::
|
||||||
|
|
||||||
|
>>> x = np.array([[0.5]])
|
||||||
|
>>> interpolateArray(data, x)
|
||||||
|
array([[ 5.5, 11. , 22. ]])
|
||||||
|
|
||||||
|
This is useful for interpolating from arrays of colors, vertexes, etc.
|
||||||
|
"""
|
||||||
|
|
||||||
|
prof = debug.Profiler()
|
||||||
|
|
||||||
|
result = np.empty(x.shape[:-1] + data.shape, dtype=data.dtype)
|
||||||
|
nd = data.ndim
|
||||||
|
md = x.shape[-1]
|
||||||
|
|
||||||
|
# First we generate arrays of indexes that are needed to
|
||||||
|
# extract the data surrounding each point
|
||||||
|
fields = np.mgrid[(slice(0,2),) * md]
|
||||||
|
xmin = np.floor(x).astype(int)
|
||||||
|
xmax = xmin + 1
|
||||||
|
indexes = np.concatenate([xmin[np.newaxis, ...], xmax[np.newaxis, ...]])
|
||||||
|
fieldInds = []
|
||||||
|
totalMask = np.ones(x.shape[:-1], dtype=bool) # keep track of out-of-bound indexes
|
||||||
|
for ax in range(md):
|
||||||
|
mask = (xmin[...,ax] >= 0) & (x[...,ax] <= data.shape[ax]-1)
|
||||||
|
# keep track of points that need to be set to default
|
||||||
|
totalMask &= mask
|
||||||
|
|
||||||
|
# ..and keep track of indexes that are out of bounds
|
||||||
|
# (note that when x[...,ax] == data.shape[ax], then xmax[...,ax] will be out
|
||||||
|
# of bounds, but the interpolation will work anyway)
|
||||||
|
mask &= (xmax[...,ax] < data.shape[ax])
|
||||||
|
axisIndex = indexes[...,ax][fields[ax]]
|
||||||
|
#axisMask = mask.astype(np.ubyte).reshape((1,)*(fields.ndim-1) + mask.shape)
|
||||||
|
axisIndex[axisIndex < 0] = 0
|
||||||
|
axisIndex[axisIndex >= data.shape[ax]] = 0
|
||||||
|
fieldInds.append(axisIndex)
|
||||||
|
prof()
|
||||||
|
|
||||||
|
# Get data values surrounding each requested point
|
||||||
|
# fieldData[..., i] contains all 2**nd values needed to interpolate x[i]
|
||||||
|
fieldData = data[tuple(fieldInds)]
|
||||||
|
prof()
|
||||||
|
|
||||||
|
## Interpolate
|
||||||
|
s = np.empty((md,) + fieldData.shape, dtype=float)
|
||||||
|
dx = x - xmin
|
||||||
|
# reshape fields for arithmetic against dx
|
||||||
|
for ax in range(md):
|
||||||
|
f1 = fields[ax].reshape(fields[ax].shape + (1,)*(dx.ndim-1))
|
||||||
|
sax = f1 * dx[...,ax] + (1-f1) * (1-dx[...,ax])
|
||||||
|
sax = sax.reshape(sax.shape + (1,) * (s.ndim-1-sax.ndim))
|
||||||
|
s[ax] = sax
|
||||||
|
s = np.product(s, axis=0)
|
||||||
|
result = fieldData * s
|
||||||
|
for i in range(md):
|
||||||
|
result = result.sum(axis=0)
|
||||||
|
|
||||||
|
prof()
|
||||||
|
totalMask.shape = totalMask.shape + (1,) * (nd - md)
|
||||||
|
result[~totalMask] = default
|
||||||
|
prof()
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
def transformToArray(tr):
|
def transformToArray(tr):
|
||||||
"""
|
"""
|
||||||
Given a QTransform, return a 3x3 numpy array.
|
Given a QTransform, return a 3x3 numpy array.
|
||||||
@ -577,17 +686,25 @@ def transformCoordinates(tr, coords, transpose=False):
|
|||||||
def solve3DTransform(points1, points2):
|
def solve3DTransform(points1, points2):
|
||||||
"""
|
"""
|
||||||
Find a 3D transformation matrix that maps points1 onto points2.
|
Find a 3D transformation matrix that maps points1 onto points2.
|
||||||
Points must be specified as a list of 4 Vectors.
|
Points must be specified as either lists of 4 Vectors or
|
||||||
|
(4, 3) arrays.
|
||||||
"""
|
"""
|
||||||
if not HAVE_SCIPY:
|
import numpy.linalg
|
||||||
raise Exception("This function depends on the scipy library, but it does not appear to be importable.")
|
pts = []
|
||||||
A = np.array([[points1[i].x(), points1[i].y(), points1[i].z(), 1] for i in range(4)])
|
for inp in (points1, points2):
|
||||||
B = np.array([[points2[i].x(), points2[i].y(), points2[i].z(), 1] for i in range(4)])
|
if isinstance(inp, np.ndarray):
|
||||||
|
A = np.empty((4,4), dtype=float)
|
||||||
|
A[:,:3] = inp[:,:3]
|
||||||
|
A[:,3] = 1.0
|
||||||
|
else:
|
||||||
|
A = np.array([[inp[i].x(), inp[i].y(), inp[i].z(), 1] for i in range(4)])
|
||||||
|
pts.append(A)
|
||||||
|
|
||||||
## solve 3 sets of linear equations to determine transformation matrix elements
|
## solve 3 sets of linear equations to determine transformation matrix elements
|
||||||
matrix = np.zeros((4,4))
|
matrix = np.zeros((4,4))
|
||||||
for i in range(3):
|
for i in range(3):
|
||||||
matrix[i] = scipy.linalg.solve(A, B[:,i]) ## solve Ax = B; x is one row of the desired transformation matrix
|
## solve Ax = B; x is one row of the desired transformation matrix
|
||||||
|
matrix[i] = numpy.linalg.solve(pts[0], pts[1][:,i])
|
||||||
|
|
||||||
return matrix
|
return matrix
|
||||||
|
|
||||||
@ -600,8 +717,7 @@ def solveBilinearTransform(points1, points2):
|
|||||||
|
|
||||||
mapped = np.dot(matrix, [x*y, x, y, 1])
|
mapped = np.dot(matrix, [x*y, x, y, 1])
|
||||||
"""
|
"""
|
||||||
if not HAVE_SCIPY:
|
import numpy.linalg
|
||||||
raise Exception("This function depends on the scipy library, but it does not appear to be importable.")
|
|
||||||
## A is 4 rows (points) x 4 columns (xy, x, y, 1)
|
## A is 4 rows (points) x 4 columns (xy, x, y, 1)
|
||||||
## B is 4 rows (points) x 2 columns (x, y)
|
## B is 4 rows (points) x 2 columns (x, y)
|
||||||
A = np.array([[points1[i].x()*points1[i].y(), points1[i].x(), points1[i].y(), 1] for i in range(4)])
|
A = np.array([[points1[i].x()*points1[i].y(), points1[i].x(), points1[i].y(), 1] for i in range(4)])
|
||||||
@ -610,7 +726,7 @@ def solveBilinearTransform(points1, points2):
|
|||||||
## solve 2 sets of linear equations to determine transformation matrix elements
|
## solve 2 sets of linear equations to determine transformation matrix elements
|
||||||
matrix = np.zeros((2,4))
|
matrix = np.zeros((2,4))
|
||||||
for i in range(2):
|
for i in range(2):
|
||||||
matrix[i] = scipy.linalg.solve(A, B[:,i]) ## solve Ax = B; x is one row of the desired transformation matrix
|
matrix[i] = numpy.linalg.solve(A, B[:,i]) ## solve Ax = B; x is one row of the desired transformation matrix
|
||||||
|
|
||||||
return matrix
|
return matrix
|
||||||
|
|
||||||
@ -629,6 +745,10 @@ def rescaleData(data, scale, offset, dtype=None):
|
|||||||
try:
|
try:
|
||||||
if not getConfigOption('useWeave'):
|
if not getConfigOption('useWeave'):
|
||||||
raise Exception('Weave is disabled; falling back to slower version.')
|
raise Exception('Weave is disabled; falling back to slower version.')
|
||||||
|
try:
|
||||||
|
import scipy.weave
|
||||||
|
except ImportError:
|
||||||
|
raise Exception('scipy.weave is not importable; falling back to slower version.')
|
||||||
|
|
||||||
## require native dtype when using weave
|
## require native dtype when using weave
|
||||||
if not data.dtype.isnative:
|
if not data.dtype.isnative:
|
||||||
@ -671,68 +791,13 @@ def applyLookupTable(data, lut):
|
|||||||
Uses values in *data* as indexes to select values from *lut*.
|
Uses values in *data* as indexes to select values from *lut*.
|
||||||
The returned data has shape data.shape + lut.shape[1:]
|
The returned data has shape data.shape + lut.shape[1:]
|
||||||
|
|
||||||
Uses scipy.weave to improve performance if it is available.
|
|
||||||
Note: color gradient lookup tables can be generated using GradientWidget.
|
Note: color gradient lookup tables can be generated using GradientWidget.
|
||||||
"""
|
"""
|
||||||
if data.dtype.kind not in ('i', 'u'):
|
if data.dtype.kind not in ('i', 'u'):
|
||||||
data = data.astype(int)
|
data = data.astype(int)
|
||||||
|
|
||||||
## using np.take appears to be faster than even the scipy.weave method and takes care of clipping as well.
|
|
||||||
return np.take(lut, data, axis=0, mode='clip')
|
return np.take(lut, data, axis=0, mode='clip')
|
||||||
|
|
||||||
### old methods:
|
|
||||||
#data = np.clip(data, 0, lut.shape[0]-1)
|
|
||||||
|
|
||||||
#try:
|
|
||||||
#if not USE_WEAVE:
|
|
||||||
#raise Exception('Weave is disabled; falling back to slower version.')
|
|
||||||
|
|
||||||
### number of values to copy for each LUT lookup
|
|
||||||
#if lut.ndim == 1:
|
|
||||||
#ncol = 1
|
|
||||||
#else:
|
|
||||||
#ncol = sum(lut.shape[1:])
|
|
||||||
|
|
||||||
### output array
|
|
||||||
#newData = np.empty((data.size, ncol), dtype=lut.dtype)
|
|
||||||
|
|
||||||
### flattened input arrays
|
|
||||||
#flatData = data.flatten()
|
|
||||||
#flatLut = lut.reshape((lut.shape[0], ncol))
|
|
||||||
|
|
||||||
#dataSize = data.size
|
|
||||||
|
|
||||||
### strides for accessing each item
|
|
||||||
#newStride = newData.strides[0] / newData.dtype.itemsize
|
|
||||||
#lutStride = flatLut.strides[0] / flatLut.dtype.itemsize
|
|
||||||
#dataStride = flatData.strides[0] / flatData.dtype.itemsize
|
|
||||||
|
|
||||||
### strides for accessing individual values within a single LUT lookup
|
|
||||||
#newColStride = newData.strides[1] / newData.dtype.itemsize
|
|
||||||
#lutColStride = flatLut.strides[1] / flatLut.dtype.itemsize
|
|
||||||
|
|
||||||
#code = """
|
|
||||||
|
|
||||||
#for( int i=0; i<dataSize; i++ ) {
|
|
||||||
#for( int j=0; j<ncol; j++ ) {
|
|
||||||
#newData[i*newStride + j*newColStride] = flatLut[flatData[i*dataStride]*lutStride + j*lutColStride];
|
|
||||||
#}
|
|
||||||
#}
|
|
||||||
#"""
|
|
||||||
#scipy.weave.inline(code, ['flatData', 'flatLut', 'newData', 'dataSize', 'ncol', 'newStride', 'lutStride', 'dataStride', 'newColStride', 'lutColStride'])
|
|
||||||
#newData = newData.reshape(data.shape + lut.shape[1:])
|
|
||||||
##if np.any(newData != lut[data]):
|
|
||||||
##print "mismatch!"
|
|
||||||
|
|
||||||
#data = newData
|
|
||||||
#except:
|
|
||||||
#if USE_WEAVE:
|
|
||||||
#debug.printExc("Error; disabling weave.")
|
|
||||||
#USE_WEAVE = False
|
|
||||||
#data = lut[data]
|
|
||||||
|
|
||||||
#return data
|
|
||||||
|
|
||||||
|
|
||||||
def makeRGBA(*args, **kwds):
|
def makeRGBA(*args, **kwds):
|
||||||
"""Equivalent to makeARGB(..., useRGBA=True)"""
|
"""Equivalent to makeARGB(..., useRGBA=True)"""
|
||||||
@ -751,36 +816,36 @@ def makeARGB(data, lut=None, levels=None, scale=None, useRGBA=False):
|
|||||||
|
|
||||||
Both stages are optional.
|
Both stages are optional.
|
||||||
|
|
||||||
============ ==================================================================================
|
============== ==================================================================================
|
||||||
Arguments:
|
**Arguments:**
|
||||||
data numpy array of int/float types. If
|
data numpy array of int/float types. If
|
||||||
levels List [min, max]; optionally rescale data before converting through the
|
levels List [min, max]; optionally rescale data before converting through the
|
||||||
lookup table. The data is rescaled such that min->0 and max->*scale*::
|
lookup table. The data is rescaled such that min->0 and max->*scale*::
|
||||||
|
|
||||||
rescaled = (clip(data, min, max) - min) * (*scale* / (max - min))
|
rescaled = (clip(data, min, max) - min) * (*scale* / (max - min))
|
||||||
|
|
||||||
It is also possible to use a 2D (N,2) array of values for levels. In this case,
|
It is also possible to use a 2D (N,2) array of values for levels. In this case,
|
||||||
it is assumed that each pair of min,max values in the levels array should be
|
it is assumed that each pair of min,max values in the levels array should be
|
||||||
applied to a different subset of the input data (for example, the input data may
|
applied to a different subset of the input data (for example, the input data may
|
||||||
already have RGB values and the levels are used to independently scale each
|
already have RGB values and the levels are used to independently scale each
|
||||||
channel). The use of this feature requires that levels.shape[0] == data.shape[-1].
|
channel). The use of this feature requires that levels.shape[0] == data.shape[-1].
|
||||||
scale The maximum value to which data will be rescaled before being passed through the
|
scale The maximum value to which data will be rescaled before being passed through the
|
||||||
lookup table (or returned if there is no lookup table). By default this will
|
lookup table (or returned if there is no lookup table). By default this will
|
||||||
be set to the length of the lookup table, or 256 is no lookup table is provided.
|
be set to the length of the lookup table, or 256 is no lookup table is provided.
|
||||||
For OpenGL color specifications (as in GLColor4f) use scale=1.0
|
For OpenGL color specifications (as in GLColor4f) use scale=1.0
|
||||||
lut Optional lookup table (array with dtype=ubyte).
|
lut Optional lookup table (array with dtype=ubyte).
|
||||||
Values in data will be converted to color by indexing directly from lut.
|
Values in data will be converted to color by indexing directly from lut.
|
||||||
The output data shape will be input.shape + lut.shape[1:].
|
The output data shape will be input.shape + lut.shape[1:].
|
||||||
|
|
||||||
Note: the output of makeARGB will have the same dtype as the lookup table, so
|
Note: the output of makeARGB will have the same dtype as the lookup table, so
|
||||||
for conversion to QImage, the dtype must be ubyte.
|
for conversion to QImage, the dtype must be ubyte.
|
||||||
|
|
||||||
Lookup tables can be built using GradientWidget.
|
Lookup tables can be built using GradientWidget.
|
||||||
useRGBA If True, the data is returned in RGBA order (useful for building OpenGL textures).
|
useRGBA If True, the data is returned in RGBA order (useful for building OpenGL textures).
|
||||||
The default is False, which returns in ARGB order for use with QImage
|
The default is False, which returns in ARGB order for use with QImage
|
||||||
(Note that 'ARGB' is a term used by the Qt documentation; the _actual_ order
|
(Note that 'ARGB' is a term used by the Qt documentation; the _actual_ order
|
||||||
is BGRA).
|
is BGRA).
|
||||||
============ ==================================================================================
|
============== ==================================================================================
|
||||||
"""
|
"""
|
||||||
profile = debug.Profiler()
|
profile = debug.Profiler()
|
||||||
|
|
||||||
@ -887,23 +952,23 @@ def makeQImage(imgData, alpha=None, copy=True, transpose=True):
|
|||||||
pointing to the array which shares its data to prevent python
|
pointing to the array which shares its data to prevent python
|
||||||
freeing that memory while the image is in use.
|
freeing that memory while the image is in use.
|
||||||
|
|
||||||
=========== ===================================================================
|
============== ===================================================================
|
||||||
Arguments:
|
**Arguments:**
|
||||||
imgData Array of data to convert. Must have shape (width, height, 3 or 4)
|
imgData Array of data to convert. Must have shape (width, height, 3 or 4)
|
||||||
and dtype=ubyte. The order of values in the 3rd axis must be
|
and dtype=ubyte. The order of values in the 3rd axis must be
|
||||||
(b, g, r, a).
|
(b, g, r, a).
|
||||||
alpha If True, the QImage returned will have format ARGB32. If False,
|
alpha If True, the QImage returned will have format ARGB32. If False,
|
||||||
the format will be RGB32. By default, _alpha_ is True if
|
the format will be RGB32. By default, _alpha_ is True if
|
||||||
array.shape[2] == 4.
|
array.shape[2] == 4.
|
||||||
copy If True, the data is copied before converting to QImage.
|
copy If True, the data is copied before converting to QImage.
|
||||||
If False, the new QImage points directly to the data in the array.
|
If False, the new QImage points directly to the data in the array.
|
||||||
Note that the array must be contiguous for this to work
|
Note that the array must be contiguous for this to work
|
||||||
(see numpy.ascontiguousarray).
|
(see numpy.ascontiguousarray).
|
||||||
transpose If True (the default), the array x/y axes are transposed before
|
transpose If True (the default), the array x/y axes are transposed before
|
||||||
creating the image. Note that Qt expects the axes to be in
|
creating the image. Note that Qt expects the axes to be in
|
||||||
(height, width) order whereas pyqtgraph usually prefers the
|
(height, width) order whereas pyqtgraph usually prefers the
|
||||||
opposite.
|
opposite.
|
||||||
=========== ===================================================================
|
============== ===================================================================
|
||||||
"""
|
"""
|
||||||
## create QImage from buffer
|
## create QImage from buffer
|
||||||
profile = debug.Profiler()
|
profile = debug.Profiler()
|
||||||
@ -993,6 +1058,10 @@ def imageToArray(img, copy=False, transpose=True):
|
|||||||
else:
|
else:
|
||||||
ptr.setsize(img.byteCount())
|
ptr.setsize(img.byteCount())
|
||||||
arr = np.asarray(ptr)
|
arr = np.asarray(ptr)
|
||||||
|
if img.byteCount() != arr.size * arr.itemsize:
|
||||||
|
# Required for Python 2.6, PyQt 4.10
|
||||||
|
# If this works on all platforms, then there is no need to use np.asarray..
|
||||||
|
arr = np.frombuffer(ptr, np.ubyte, img.byteCount())
|
||||||
|
|
||||||
if fmt == img.Format_RGB32:
|
if fmt == img.Format_RGB32:
|
||||||
arr = arr.reshape(img.height(), img.width(), 3)
|
arr = arr.reshape(img.height(), img.width(), 3)
|
||||||
@ -1051,7 +1120,86 @@ def colorToAlpha(data, color):
|
|||||||
|
|
||||||
#raise Exception()
|
#raise Exception()
|
||||||
return np.clip(output, 0, 255).astype(np.ubyte)
|
return np.clip(output, 0, 255).astype(np.ubyte)
|
||||||
|
|
||||||
|
def gaussianFilter(data, sigma):
|
||||||
|
"""
|
||||||
|
Drop-in replacement for scipy.ndimage.gaussian_filter.
|
||||||
|
|
||||||
|
(note: results are only approximately equal to the output of
|
||||||
|
gaussian_filter)
|
||||||
|
"""
|
||||||
|
if np.isscalar(sigma):
|
||||||
|
sigma = (sigma,) * data.ndim
|
||||||
|
|
||||||
|
baseline = data.mean()
|
||||||
|
filtered = data - baseline
|
||||||
|
for ax in range(data.ndim):
|
||||||
|
s = sigma[ax]
|
||||||
|
if s == 0:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# generate 1D gaussian kernel
|
||||||
|
ksize = int(s * 6)
|
||||||
|
x = np.arange(-ksize, ksize)
|
||||||
|
kernel = np.exp(-x**2 / (2*s**2))
|
||||||
|
kshape = [1,] * data.ndim
|
||||||
|
kshape[ax] = len(kernel)
|
||||||
|
kernel = kernel.reshape(kshape)
|
||||||
|
|
||||||
|
# convolve as product of FFTs
|
||||||
|
shape = data.shape[ax] + ksize
|
||||||
|
scale = 1.0 / (abs(s) * (2*np.pi)**0.5)
|
||||||
|
filtered = scale * np.fft.irfft(np.fft.rfft(filtered, shape, axis=ax) *
|
||||||
|
np.fft.rfft(kernel, shape, axis=ax),
|
||||||
|
axis=ax)
|
||||||
|
|
||||||
|
# clip off extra data
|
||||||
|
sl = [slice(None)] * data.ndim
|
||||||
|
sl[ax] = slice(filtered.shape[ax]-data.shape[ax],None,None)
|
||||||
|
filtered = filtered[sl]
|
||||||
|
return filtered + baseline
|
||||||
|
|
||||||
|
|
||||||
|
def downsample(data, n, axis=0, xvals='subsample'):
|
||||||
|
"""Downsample by averaging points together across axis.
|
||||||
|
If multiple axes are specified, runs once per axis.
|
||||||
|
If a metaArray is given, then the axis values can be either subsampled
|
||||||
|
or downsampled to match.
|
||||||
|
"""
|
||||||
|
ma = None
|
||||||
|
if (hasattr(data, 'implements') and data.implements('MetaArray')):
|
||||||
|
ma = data
|
||||||
|
data = data.view(np.ndarray)
|
||||||
|
|
||||||
|
|
||||||
|
if hasattr(axis, '__len__'):
|
||||||
|
if not hasattr(n, '__len__'):
|
||||||
|
n = [n]*len(axis)
|
||||||
|
for i in range(len(axis)):
|
||||||
|
data = downsample(data, n[i], axis[i])
|
||||||
|
return data
|
||||||
|
|
||||||
|
nPts = int(data.shape[axis] / n)
|
||||||
|
s = list(data.shape)
|
||||||
|
s[axis] = nPts
|
||||||
|
s.insert(axis+1, n)
|
||||||
|
sl = [slice(None)] * data.ndim
|
||||||
|
sl[axis] = slice(0, nPts*n)
|
||||||
|
d1 = data[tuple(sl)]
|
||||||
|
#print d1.shape, s
|
||||||
|
d1.shape = tuple(s)
|
||||||
|
d2 = d1.mean(axis+1)
|
||||||
|
|
||||||
|
if ma is None:
|
||||||
|
return d2
|
||||||
|
else:
|
||||||
|
info = ma.infoCopy()
|
||||||
|
if 'values' in info[axis]:
|
||||||
|
if xvals == 'subsample':
|
||||||
|
info[axis]['values'] = info[axis]['values'][::n][:nPts]
|
||||||
|
elif xvals == 'downsample':
|
||||||
|
info[axis]['values'] = downsample(info[axis]['values'], n)
|
||||||
|
return MetaArray(d2, info=info)
|
||||||
|
|
||||||
|
|
||||||
def arrayToQPath(x, y, connect='all'):
|
def arrayToQPath(x, y, connect='all'):
|
||||||
@ -1113,6 +1261,8 @@ def arrayToQPath(x, y, connect='all'):
|
|||||||
# decide which points are connected by lines
|
# decide which points are connected by lines
|
||||||
if connect == 'pairs':
|
if connect == 'pairs':
|
||||||
connect = np.empty((n/2,2), dtype=np.int32)
|
connect = np.empty((n/2,2), dtype=np.int32)
|
||||||
|
if connect.size != n:
|
||||||
|
raise Exception("x,y array lengths must be multiple of 2 to use connect='pairs'")
|
||||||
connect[:,0] = 1
|
connect[:,0] = 1
|
||||||
connect[:,1] = 0
|
connect[:,1] = 0
|
||||||
connect = connect.flatten()
|
connect = connect.flatten()
|
||||||
@ -1240,19 +1390,19 @@ def isocurve(data, level, connected=False, extendToEdge=False, path=False):
|
|||||||
"""
|
"""
|
||||||
Generate isocurve from 2D data using marching squares algorithm.
|
Generate isocurve from 2D data using marching squares algorithm.
|
||||||
|
|
||||||
============= =========================================================
|
============== =========================================================
|
||||||
Arguments
|
**Arguments:**
|
||||||
data 2D numpy array of scalar values
|
data 2D numpy array of scalar values
|
||||||
level The level at which to generate an isosurface
|
level The level at which to generate an isosurface
|
||||||
connected If False, return a single long list of point pairs
|
connected If False, return a single long list of point pairs
|
||||||
If True, return multiple long lists of connected point
|
If True, return multiple long lists of connected point
|
||||||
locations. (This is slower but better for drawing
|
locations. (This is slower but better for drawing
|
||||||
continuous lines)
|
continuous lines)
|
||||||
extendToEdge If True, extend the curves to reach the exact edges of
|
extendToEdge If True, extend the curves to reach the exact edges of
|
||||||
the data.
|
the data.
|
||||||
path if True, return a QPainterPath rather than a list of
|
path if True, return a QPainterPath rather than a list of
|
||||||
vertex coordinates. This forces connected=True.
|
vertex coordinates. This forces connected=True.
|
||||||
============= =========================================================
|
============== =========================================================
|
||||||
|
|
||||||
This function is SLOW; plenty of room for optimization here.
|
This function is SLOW; plenty of room for optimization here.
|
||||||
"""
|
"""
|
||||||
@ -1274,30 +1424,30 @@ def isocurve(data, level, connected=False, extendToEdge=False, path=False):
|
|||||||
data = d2
|
data = d2
|
||||||
|
|
||||||
sideTable = [
|
sideTable = [
|
||||||
[],
|
[],
|
||||||
[0,1],
|
[0,1],
|
||||||
[1,2],
|
[1,2],
|
||||||
[0,2],
|
[0,2],
|
||||||
[0,3],
|
[0,3],
|
||||||
[1,3],
|
[1,3],
|
||||||
[0,1,2,3],
|
[0,1,2,3],
|
||||||
[2,3],
|
[2,3],
|
||||||
[2,3],
|
[2,3],
|
||||||
[0,1,2,3],
|
[0,1,2,3],
|
||||||
[1,3],
|
[1,3],
|
||||||
[0,3],
|
[0,3],
|
||||||
[0,2],
|
[0,2],
|
||||||
[1,2],
|
[1,2],
|
||||||
[0,1],
|
[0,1],
|
||||||
[]
|
[]
|
||||||
]
|
]
|
||||||
|
|
||||||
edgeKey=[
|
edgeKey=[
|
||||||
[(0,1), (0,0)],
|
[(0,1), (0,0)],
|
||||||
[(0,0), (1,0)],
|
[(0,0), (1,0)],
|
||||||
[(1,0), (1,1)],
|
[(1,0), (1,1)],
|
||||||
[(1,1), (0,1)]
|
[(1,1), (0,1)]
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
lines = []
|
lines = []
|
||||||
@ -1427,7 +1577,11 @@ def traceImage(image, values, smooth=0.5):
|
|||||||
If image is RGB or RGBA, then the shape of values should be (nvals, 3/4)
|
If image is RGB or RGBA, then the shape of values should be (nvals, 3/4)
|
||||||
The parameter *smooth* is expressed in pixels.
|
The parameter *smooth* is expressed in pixels.
|
||||||
"""
|
"""
|
||||||
import scipy.ndimage as ndi
|
try:
|
||||||
|
import scipy.ndimage as ndi
|
||||||
|
except ImportError:
|
||||||
|
raise Exception("traceImage() requires the package scipy.ndimage, but it is not importable.")
|
||||||
|
|
||||||
if values.ndim == 2:
|
if values.ndim == 2:
|
||||||
values = values.T
|
values = values.T
|
||||||
values = values[np.newaxis, np.newaxis, ...].astype(float)
|
values = values[np.newaxis, np.newaxis, ...].astype(float)
|
||||||
@ -1441,7 +1595,7 @@ def traceImage(image, values, smooth=0.5):
|
|||||||
paths = []
|
paths = []
|
||||||
for i in range(diff.shape[-1]):
|
for i in range(diff.shape[-1]):
|
||||||
d = (labels==i).astype(float)
|
d = (labels==i).astype(float)
|
||||||
d = ndi.gaussian_filter(d, (smooth, smooth))
|
d = gaussianFilter(d, (smooth, smooth))
|
||||||
lines = isocurve(d, 0.5, connected=True, extendToEdge=True)
|
lines = isocurve(d, 0.5, connected=True, extendToEdge=True)
|
||||||
path = QtGui.QPainterPath()
|
path = QtGui.QPainterPath()
|
||||||
for line in lines:
|
for line in lines:
|
||||||
@ -1481,38 +1635,39 @@ def isosurface(data, level):
|
|||||||
## edge index tells us which edges are cut by the isosurface.
|
## edge index tells us which edges are cut by the isosurface.
|
||||||
## (Data stolen from Bourk; see above.)
|
## (Data stolen from Bourk; see above.)
|
||||||
edgeTable = np.array([
|
edgeTable = np.array([
|
||||||
0x0 , 0x109, 0x203, 0x30a, 0x406, 0x50f, 0x605, 0x70c,
|
0x0 , 0x109, 0x203, 0x30a, 0x406, 0x50f, 0x605, 0x70c,
|
||||||
0x80c, 0x905, 0xa0f, 0xb06, 0xc0a, 0xd03, 0xe09, 0xf00,
|
0x80c, 0x905, 0xa0f, 0xb06, 0xc0a, 0xd03, 0xe09, 0xf00,
|
||||||
0x190, 0x99 , 0x393, 0x29a, 0x596, 0x49f, 0x795, 0x69c,
|
0x190, 0x99 , 0x393, 0x29a, 0x596, 0x49f, 0x795, 0x69c,
|
||||||
0x99c, 0x895, 0xb9f, 0xa96, 0xd9a, 0xc93, 0xf99, 0xe90,
|
0x99c, 0x895, 0xb9f, 0xa96, 0xd9a, 0xc93, 0xf99, 0xe90,
|
||||||
0x230, 0x339, 0x33 , 0x13a, 0x636, 0x73f, 0x435, 0x53c,
|
0x230, 0x339, 0x33 , 0x13a, 0x636, 0x73f, 0x435, 0x53c,
|
||||||
0xa3c, 0xb35, 0x83f, 0x936, 0xe3a, 0xf33, 0xc39, 0xd30,
|
0xa3c, 0xb35, 0x83f, 0x936, 0xe3a, 0xf33, 0xc39, 0xd30,
|
||||||
0x3a0, 0x2a9, 0x1a3, 0xaa , 0x7a6, 0x6af, 0x5a5, 0x4ac,
|
0x3a0, 0x2a9, 0x1a3, 0xaa , 0x7a6, 0x6af, 0x5a5, 0x4ac,
|
||||||
0xbac, 0xaa5, 0x9af, 0x8a6, 0xfaa, 0xea3, 0xda9, 0xca0,
|
0xbac, 0xaa5, 0x9af, 0x8a6, 0xfaa, 0xea3, 0xda9, 0xca0,
|
||||||
0x460, 0x569, 0x663, 0x76a, 0x66 , 0x16f, 0x265, 0x36c,
|
0x460, 0x569, 0x663, 0x76a, 0x66 , 0x16f, 0x265, 0x36c,
|
||||||
0xc6c, 0xd65, 0xe6f, 0xf66, 0x86a, 0x963, 0xa69, 0xb60,
|
0xc6c, 0xd65, 0xe6f, 0xf66, 0x86a, 0x963, 0xa69, 0xb60,
|
||||||
0x5f0, 0x4f9, 0x7f3, 0x6fa, 0x1f6, 0xff , 0x3f5, 0x2fc,
|
0x5f0, 0x4f9, 0x7f3, 0x6fa, 0x1f6, 0xff , 0x3f5, 0x2fc,
|
||||||
0xdfc, 0xcf5, 0xfff, 0xef6, 0x9fa, 0x8f3, 0xbf9, 0xaf0,
|
0xdfc, 0xcf5, 0xfff, 0xef6, 0x9fa, 0x8f3, 0xbf9, 0xaf0,
|
||||||
0x650, 0x759, 0x453, 0x55a, 0x256, 0x35f, 0x55 , 0x15c,
|
0x650, 0x759, 0x453, 0x55a, 0x256, 0x35f, 0x55 , 0x15c,
|
||||||
0xe5c, 0xf55, 0xc5f, 0xd56, 0xa5a, 0xb53, 0x859, 0x950,
|
0xe5c, 0xf55, 0xc5f, 0xd56, 0xa5a, 0xb53, 0x859, 0x950,
|
||||||
0x7c0, 0x6c9, 0x5c3, 0x4ca, 0x3c6, 0x2cf, 0x1c5, 0xcc ,
|
0x7c0, 0x6c9, 0x5c3, 0x4ca, 0x3c6, 0x2cf, 0x1c5, 0xcc ,
|
||||||
0xfcc, 0xec5, 0xdcf, 0xcc6, 0xbca, 0xac3, 0x9c9, 0x8c0,
|
0xfcc, 0xec5, 0xdcf, 0xcc6, 0xbca, 0xac3, 0x9c9, 0x8c0,
|
||||||
0x8c0, 0x9c9, 0xac3, 0xbca, 0xcc6, 0xdcf, 0xec5, 0xfcc,
|
0x8c0, 0x9c9, 0xac3, 0xbca, 0xcc6, 0xdcf, 0xec5, 0xfcc,
|
||||||
0xcc , 0x1c5, 0x2cf, 0x3c6, 0x4ca, 0x5c3, 0x6c9, 0x7c0,
|
0xcc , 0x1c5, 0x2cf, 0x3c6, 0x4ca, 0x5c3, 0x6c9, 0x7c0,
|
||||||
0x950, 0x859, 0xb53, 0xa5a, 0xd56, 0xc5f, 0xf55, 0xe5c,
|
0x950, 0x859, 0xb53, 0xa5a, 0xd56, 0xc5f, 0xf55, 0xe5c,
|
||||||
0x15c, 0x55 , 0x35f, 0x256, 0x55a, 0x453, 0x759, 0x650,
|
0x15c, 0x55 , 0x35f, 0x256, 0x55a, 0x453, 0x759, 0x650,
|
||||||
0xaf0, 0xbf9, 0x8f3, 0x9fa, 0xef6, 0xfff, 0xcf5, 0xdfc,
|
0xaf0, 0xbf9, 0x8f3, 0x9fa, 0xef6, 0xfff, 0xcf5, 0xdfc,
|
||||||
0x2fc, 0x3f5, 0xff , 0x1f6, 0x6fa, 0x7f3, 0x4f9, 0x5f0,
|
0x2fc, 0x3f5, 0xff , 0x1f6, 0x6fa, 0x7f3, 0x4f9, 0x5f0,
|
||||||
0xb60, 0xa69, 0x963, 0x86a, 0xf66, 0xe6f, 0xd65, 0xc6c,
|
0xb60, 0xa69, 0x963, 0x86a, 0xf66, 0xe6f, 0xd65, 0xc6c,
|
||||||
0x36c, 0x265, 0x16f, 0x66 , 0x76a, 0x663, 0x569, 0x460,
|
0x36c, 0x265, 0x16f, 0x66 , 0x76a, 0x663, 0x569, 0x460,
|
||||||
0xca0, 0xda9, 0xea3, 0xfaa, 0x8a6, 0x9af, 0xaa5, 0xbac,
|
0xca0, 0xda9, 0xea3, 0xfaa, 0x8a6, 0x9af, 0xaa5, 0xbac,
|
||||||
0x4ac, 0x5a5, 0x6af, 0x7a6, 0xaa , 0x1a3, 0x2a9, 0x3a0,
|
0x4ac, 0x5a5, 0x6af, 0x7a6, 0xaa , 0x1a3, 0x2a9, 0x3a0,
|
||||||
0xd30, 0xc39, 0xf33, 0xe3a, 0x936, 0x83f, 0xb35, 0xa3c,
|
0xd30, 0xc39, 0xf33, 0xe3a, 0x936, 0x83f, 0xb35, 0xa3c,
|
||||||
0x53c, 0x435, 0x73f, 0x636, 0x13a, 0x33 , 0x339, 0x230,
|
0x53c, 0x435, 0x73f, 0x636, 0x13a, 0x33 , 0x339, 0x230,
|
||||||
0xe90, 0xf99, 0xc93, 0xd9a, 0xa96, 0xb9f, 0x895, 0x99c,
|
0xe90, 0xf99, 0xc93, 0xd9a, 0xa96, 0xb9f, 0x895, 0x99c,
|
||||||
0x69c, 0x795, 0x49f, 0x596, 0x29a, 0x393, 0x99 , 0x190,
|
0x69c, 0x795, 0x49f, 0x596, 0x29a, 0x393, 0x99 , 0x190,
|
||||||
0xf00, 0xe09, 0xd03, 0xc0a, 0xb06, 0xa0f, 0x905, 0x80c,
|
0xf00, 0xe09, 0xd03, 0xc0a, 0xb06, 0xa0f, 0x905, 0x80c,
|
||||||
0x70c, 0x605, 0x50f, 0x406, 0x30a, 0x203, 0x109, 0x0 ], dtype=np.uint16)
|
0x70c, 0x605, 0x50f, 0x406, 0x30a, 0x203, 0x109, 0x0
|
||||||
|
], dtype=np.uint16)
|
||||||
|
|
||||||
## Table of triangles to use for filling each grid cell.
|
## Table of triangles to use for filling each grid cell.
|
||||||
## Each set of three integers tells us which three edges to
|
## Each set of three integers tells us which three edges to
|
||||||
@ -1790,7 +1945,7 @@ def isosurface(data, level):
|
|||||||
[1, 1, 0, 2],
|
[1, 1, 0, 2],
|
||||||
[0, 1, 0, 2],
|
[0, 1, 0, 2],
|
||||||
#[9, 9, 9, 9] ## fake
|
#[9, 9, 9, 9] ## fake
|
||||||
], dtype=np.ubyte)
|
], dtype=np.uint16) # don't use ubyte here! This value gets added to cell index later; will need the extra precision.
|
||||||
nTableFaces = np.array([len(f)/3 for f in triTable], dtype=np.ubyte)
|
nTableFaces = np.array([len(f)/3 for f in triTable], dtype=np.ubyte)
|
||||||
faceShiftTables = [None]
|
faceShiftTables = [None]
|
||||||
for i in range(1,6):
|
for i in range(1,6):
|
||||||
@ -1889,7 +2044,6 @@ def isosurface(data, level):
|
|||||||
#profiler()
|
#profiler()
|
||||||
if cells.shape[0] == 0:
|
if cells.shape[0] == 0:
|
||||||
continue
|
continue
|
||||||
#cellInds = index[(cells*ins[np.newaxis,:]).sum(axis=1)]
|
|
||||||
cellInds = index[cells[:,0], cells[:,1], cells[:,2]] ## index values of cells to process for this round
|
cellInds = index[cells[:,0], cells[:,1], cells[:,2]] ## index values of cells to process for this round
|
||||||
#profiler()
|
#profiler()
|
||||||
|
|
||||||
@ -1901,9 +2055,7 @@ def isosurface(data, level):
|
|||||||
#profiler()
|
#profiler()
|
||||||
|
|
||||||
### expensive:
|
### expensive:
|
||||||
#print verts.shape
|
|
||||||
verts = (verts * cs[np.newaxis, np.newaxis, :]).sum(axis=2)
|
verts = (verts * cs[np.newaxis, np.newaxis, :]).sum(axis=2)
|
||||||
#vertInds = cutEdges[verts[...,0], verts[...,1], verts[...,2], verts[...,3]] ## and these are the vertex indexes we want.
|
|
||||||
vertInds = cutEdges[verts]
|
vertInds = cutEdges[verts]
|
||||||
#profiler()
|
#profiler()
|
||||||
nv = vertInds.shape[0]
|
nv = vertInds.shape[0]
|
||||||
@ -1924,14 +2076,16 @@ def invertQTransform(tr):
|
|||||||
bugs in that method. (specifically, Qt has floating-point precision issues
|
bugs in that method. (specifically, Qt has floating-point precision issues
|
||||||
when determining whether a matrix is invertible)
|
when determining whether a matrix is invertible)
|
||||||
"""
|
"""
|
||||||
if not HAVE_SCIPY:
|
try:
|
||||||
|
import numpy.linalg
|
||||||
|
arr = np.array([[tr.m11(), tr.m12(), tr.m13()], [tr.m21(), tr.m22(), tr.m23()], [tr.m31(), tr.m32(), tr.m33()]])
|
||||||
|
inv = numpy.linalg.inv(arr)
|
||||||
|
return QtGui.QTransform(inv[0,0], inv[0,1], inv[0,2], inv[1,0], inv[1,1], inv[1,2], inv[2,0], inv[2,1])
|
||||||
|
except ImportError:
|
||||||
inv = tr.inverted()
|
inv = tr.inverted()
|
||||||
if inv[1] is False:
|
if inv[1] is False:
|
||||||
raise Exception("Transform is not invertible.")
|
raise Exception("Transform is not invertible.")
|
||||||
return inv[0]
|
return inv[0]
|
||||||
arr = np.array([[tr.m11(), tr.m12(), tr.m13()], [tr.m21(), tr.m22(), tr.m23()], [tr.m31(), tr.m32(), tr.m33()]])
|
|
||||||
inv = scipy.linalg.inv(arr)
|
|
||||||
return QtGui.QTransform(inv[0,0], inv[0,1], inv[0,2], inv[1,0], inv[1,1], inv[1,2], inv[2,0], inv[2,1])
|
|
||||||
|
|
||||||
|
|
||||||
def pseudoScatter(data, spacing=None, shuffle=True, bidir=False):
|
def pseudoScatter(data, spacing=None, shuffle=True, bidir=False):
|
||||||
|
@ -16,12 +16,14 @@ class ArrowItem(QtGui.QGraphicsPathItem):
|
|||||||
Arrows can be initialized with any keyword arguments accepted by
|
Arrows can be initialized with any keyword arguments accepted by
|
||||||
the setStyle() method.
|
the setStyle() method.
|
||||||
"""
|
"""
|
||||||
|
self.opts = {}
|
||||||
QtGui.QGraphicsPathItem.__init__(self, opts.get('parent', None))
|
QtGui.QGraphicsPathItem.__init__(self, opts.get('parent', None))
|
||||||
|
|
||||||
if 'size' in opts:
|
if 'size' in opts:
|
||||||
opts['headLen'] = opts['size']
|
opts['headLen'] = opts['size']
|
||||||
if 'width' in opts:
|
if 'width' in opts:
|
||||||
opts['headWidth'] = opts['width']
|
opts['headWidth'] = opts['width']
|
||||||
defOpts = {
|
defaultOpts = {
|
||||||
'pxMode': True,
|
'pxMode': True,
|
||||||
'angle': -150, ## If the angle is 0, the arrow points left
|
'angle': -150, ## If the angle is 0, the arrow points left
|
||||||
'pos': (0,0),
|
'pos': (0,0),
|
||||||
@ -33,12 +35,9 @@ class ArrowItem(QtGui.QGraphicsPathItem):
|
|||||||
'pen': (200,200,200),
|
'pen': (200,200,200),
|
||||||
'brush': (50,50,200),
|
'brush': (50,50,200),
|
||||||
}
|
}
|
||||||
defOpts.update(opts)
|
defaultOpts.update(opts)
|
||||||
|
|
||||||
self.setStyle(**defOpts)
|
self.setStyle(**defaultOpts)
|
||||||
|
|
||||||
self.setPen(fn.mkPen(defOpts['pen']))
|
|
||||||
self.setBrush(fn.mkBrush(defOpts['brush']))
|
|
||||||
|
|
||||||
self.rotate(self.opts['angle'])
|
self.rotate(self.opts['angle'])
|
||||||
self.moveBy(*self.opts['pos'])
|
self.moveBy(*self.opts['pos'])
|
||||||
@ -48,35 +47,38 @@ class ArrowItem(QtGui.QGraphicsPathItem):
|
|||||||
Changes the appearance of the arrow.
|
Changes the appearance of the arrow.
|
||||||
All arguments are optional:
|
All arguments are optional:
|
||||||
|
|
||||||
================= =================================================
|
====================== =================================================
|
||||||
Keyword Arguments
|
**Keyword Arguments:**
|
||||||
angle Orientation of the arrow in degrees. Default is
|
angle Orientation of the arrow in degrees. Default is
|
||||||
0; arrow pointing to the left.
|
0; arrow pointing to the left.
|
||||||
headLen Length of the arrow head, from tip to base.
|
headLen Length of the arrow head, from tip to base.
|
||||||
default=20
|
default=20
|
||||||
headWidth Width of the arrow head at its base.
|
headWidth Width of the arrow head at its base.
|
||||||
tipAngle Angle of the tip of the arrow in degrees. Smaller
|
tipAngle Angle of the tip of the arrow in degrees. Smaller
|
||||||
values make a 'sharper' arrow. If tipAngle is
|
values make a 'sharper' arrow. If tipAngle is
|
||||||
specified, ot overrides headWidth. default=25
|
specified, ot overrides headWidth. default=25
|
||||||
baseAngle Angle of the base of the arrow head. Default is
|
baseAngle Angle of the base of the arrow head. Default is
|
||||||
0, which means that the base of the arrow head
|
0, which means that the base of the arrow head
|
||||||
is perpendicular to the arrow shaft.
|
is perpendicular to the arrow tail.
|
||||||
tailLen Length of the arrow tail, measured from the base
|
tailLen Length of the arrow tail, measured from the base
|
||||||
of the arrow head to the tip of the tail. If
|
of the arrow head to the end of the tail. If
|
||||||
this value is None, no tail will be drawn.
|
this value is None, no tail will be drawn.
|
||||||
default=None
|
default=None
|
||||||
tailWidth Width of the tail. default=3
|
tailWidth Width of the tail. default=3
|
||||||
pen The pen used to draw the outline of the arrow.
|
pen The pen used to draw the outline of the arrow.
|
||||||
brush The brush used to fill the arrow.
|
brush The brush used to fill the arrow.
|
||||||
================= =================================================
|
====================== =================================================
|
||||||
"""
|
"""
|
||||||
self.opts = opts
|
self.opts.update(opts)
|
||||||
|
|
||||||
opt = dict([(k,self.opts[k]) for k in ['headLen', 'tipAngle', 'baseAngle', 'tailLen', 'tailWidth']])
|
opt = dict([(k,self.opts[k]) for k in ['headLen', 'tipAngle', 'baseAngle', 'tailLen', 'tailWidth']])
|
||||||
self.path = fn.makeArrowPath(**opt)
|
self.path = fn.makeArrowPath(**opt)
|
||||||
self.setPath(self.path)
|
self.setPath(self.path)
|
||||||
|
|
||||||
if opts['pxMode']:
|
self.setPen(fn.mkPen(self.opts['pen']))
|
||||||
|
self.setBrush(fn.mkBrush(self.opts['brush']))
|
||||||
|
|
||||||
|
if self.opts['pxMode']:
|
||||||
self.setFlags(self.flags() | self.ItemIgnoresTransformations)
|
self.setFlags(self.flags() | self.ItemIgnoresTransformations)
|
||||||
else:
|
else:
|
||||||
self.setFlags(self.flags() & ~self.ItemIgnoresTransformations)
|
self.setFlags(self.flags() & ~self.ItemIgnoresTransformations)
|
||||||
@ -121,4 +123,4 @@ class ArrowItem(QtGui.QGraphicsPathItem):
|
|||||||
return pad
|
return pad
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@ -33,7 +33,6 @@ class AxisItem(GraphicsWidget):
|
|||||||
|
|
||||||
GraphicsWidget.__init__(self, parent)
|
GraphicsWidget.__init__(self, parent)
|
||||||
self.label = QtGui.QGraphicsTextItem(self)
|
self.label = QtGui.QGraphicsTextItem(self)
|
||||||
self.showValues = showValues
|
|
||||||
self.picture = None
|
self.picture = None
|
||||||
self.orientation = orientation
|
self.orientation = orientation
|
||||||
if orientation not in ['left', 'right', 'top', 'bottom']:
|
if orientation not in ['left', 'right', 'top', 'bottom']:
|
||||||
@ -42,7 +41,7 @@ class AxisItem(GraphicsWidget):
|
|||||||
self.label.rotate(-90)
|
self.label.rotate(-90)
|
||||||
|
|
||||||
self.style = {
|
self.style = {
|
||||||
'tickTextOffset': (5, 2), ## (horizontal, vertical) spacing between text and axis
|
'tickTextOffset': [5, 2], ## (horizontal, vertical) spacing between text and axis
|
||||||
'tickTextWidth': 30, ## space reserved for tick text
|
'tickTextWidth': 30, ## space reserved for tick text
|
||||||
'tickTextHeight': 18,
|
'tickTextHeight': 18,
|
||||||
'autoExpandTextSpace': True, ## automatically expand text space if needed
|
'autoExpandTextSpace': True, ## automatically expand text space if needed
|
||||||
@ -53,7 +52,9 @@ class AxisItem(GraphicsWidget):
|
|||||||
(2, 0.6), ## If we already have 2 ticks with text, fill no more than 60% of the axis
|
(2, 0.6), ## If we already have 2 ticks with text, fill no more than 60% of the axis
|
||||||
(4, 0.4), ## If we already have 4 ticks with text, fill no more than 40% of the axis
|
(4, 0.4), ## If we already have 4 ticks with text, fill no more than 40% of the axis
|
||||||
(6, 0.2), ## If we already have 6 ticks with text, fill no more than 20% of the axis
|
(6, 0.2), ## If we already have 6 ticks with text, fill no more than 20% of the axis
|
||||||
]
|
],
|
||||||
|
'showValues': showValues,
|
||||||
|
'tickLength': maxTickLength,
|
||||||
}
|
}
|
||||||
|
|
||||||
self.textWidth = 30 ## Keeps track of maximum width / height of tick text
|
self.textWidth = 30 ## Keeps track of maximum width / height of tick text
|
||||||
@ -66,7 +67,6 @@ class AxisItem(GraphicsWidget):
|
|||||||
self.logMode = False
|
self.logMode = False
|
||||||
self.tickFont = None
|
self.tickFont = None
|
||||||
|
|
||||||
self.tickLength = maxTickLength
|
|
||||||
self._tickLevels = None ## used to override the automatic ticking system with explicit ticks
|
self._tickLevels = None ## used to override the automatic ticking system with explicit ticks
|
||||||
self.scale = 1.0
|
self.scale = 1.0
|
||||||
self.autoSIPrefix = True
|
self.autoSIPrefix = True
|
||||||
@ -74,7 +74,10 @@ class AxisItem(GraphicsWidget):
|
|||||||
|
|
||||||
self.setRange(0, 1)
|
self.setRange(0, 1)
|
||||||
|
|
||||||
self.setPen(pen)
|
if pen is None:
|
||||||
|
self.setPen()
|
||||||
|
else:
|
||||||
|
self.setPen(pen)
|
||||||
|
|
||||||
self._linkedView = None
|
self._linkedView = None
|
||||||
if linkView is not None:
|
if linkView is not None:
|
||||||
@ -84,6 +87,73 @@ class AxisItem(GraphicsWidget):
|
|||||||
|
|
||||||
self.grid = False
|
self.grid = False
|
||||||
#self.setCacheMode(self.DeviceCoordinateCache)
|
#self.setCacheMode(self.DeviceCoordinateCache)
|
||||||
|
|
||||||
|
def setStyle(self, **kwds):
|
||||||
|
"""
|
||||||
|
Set various style options.
|
||||||
|
|
||||||
|
=================== =======================================================
|
||||||
|
Keyword Arguments:
|
||||||
|
tickLength (int) The maximum length of ticks in pixels.
|
||||||
|
Positive values point toward the text; negative
|
||||||
|
values point away.
|
||||||
|
tickTextOffset (int) reserved spacing between text and axis in px
|
||||||
|
tickTextWidth (int) Horizontal space reserved for tick text in px
|
||||||
|
tickTextHeight (int) Vertical space reserved for tick text in px
|
||||||
|
autoExpandTextSpace (bool) Automatically expand text space if the tick
|
||||||
|
strings become too long.
|
||||||
|
tickFont (QFont or None) Determines the font used for tick
|
||||||
|
values. Use None for the default font.
|
||||||
|
stopAxisAtTick (tuple: (bool min, bool max)) If True, the axis
|
||||||
|
line is drawn only as far as the last tick.
|
||||||
|
Otherwise, the line is drawn to the edge of the
|
||||||
|
AxisItem boundary.
|
||||||
|
textFillLimits (list of (tick #, % fill) tuples). This structure
|
||||||
|
determines how the AxisItem decides how many ticks
|
||||||
|
should have text appear next to them. Each tuple in
|
||||||
|
the list specifies what fraction of the axis length
|
||||||
|
may be occupied by text, given the number of ticks
|
||||||
|
that already have text displayed. For example::
|
||||||
|
|
||||||
|
[(0, 0.8), # Never fill more than 80% of the axis
|
||||||
|
(2, 0.6), # If we already have 2 ticks with text,
|
||||||
|
# fill no more than 60% of the axis
|
||||||
|
(4, 0.4), # If we already have 4 ticks with text,
|
||||||
|
# fill no more than 40% of the axis
|
||||||
|
(6, 0.2)] # If we already have 6 ticks with text,
|
||||||
|
# fill no more than 20% of the axis
|
||||||
|
|
||||||
|
showValues (bool) indicates whether text is displayed adjacent
|
||||||
|
to ticks.
|
||||||
|
=================== =======================================================
|
||||||
|
|
||||||
|
Added in version 0.9.9
|
||||||
|
"""
|
||||||
|
for kwd,value in kwds.items():
|
||||||
|
if kwd not in self.style:
|
||||||
|
raise NameError("%s is not a valid style argument." % kwd)
|
||||||
|
|
||||||
|
if kwd in ('tickLength', 'tickTextOffset', 'tickTextWidth', 'tickTextHeight'):
|
||||||
|
if not isinstance(value, int):
|
||||||
|
raise ValueError("Argument '%s' must be int" % kwd)
|
||||||
|
|
||||||
|
if kwd == 'tickTextOffset':
|
||||||
|
if self.orientation in ('left', 'right'):
|
||||||
|
self.style['tickTextOffset'][0] = value
|
||||||
|
else:
|
||||||
|
self.style['tickTextOffset'][1] = value
|
||||||
|
elif kwd == 'stopAxisAtTick':
|
||||||
|
try:
|
||||||
|
assert len(value) == 2 and isinstance(value[0], bool) and isinstance(value[1], bool)
|
||||||
|
except:
|
||||||
|
raise ValueError("Argument 'stopAxisAtTick' must have type (bool, bool)")
|
||||||
|
self.style[kwd] = value
|
||||||
|
else:
|
||||||
|
self.style[kwd] = value
|
||||||
|
|
||||||
|
self.picture = None
|
||||||
|
self._adjustSize()
|
||||||
|
self.update()
|
||||||
|
|
||||||
def close(self):
|
def close(self):
|
||||||
self.scene().removeItem(self.label)
|
self.scene().removeItem(self.label)
|
||||||
@ -125,20 +195,15 @@ class AxisItem(GraphicsWidget):
|
|||||||
if self.orientation == 'left':
|
if self.orientation == 'left':
|
||||||
p.setY(int(self.size().height()/2 + br.width()/2))
|
p.setY(int(self.size().height()/2 + br.width()/2))
|
||||||
p.setX(-nudge)
|
p.setX(-nudge)
|
||||||
#s.setWidth(10)
|
|
||||||
elif self.orientation == 'right':
|
elif self.orientation == 'right':
|
||||||
#s.setWidth(10)
|
|
||||||
p.setY(int(self.size().height()/2 + br.width()/2))
|
p.setY(int(self.size().height()/2 + br.width()/2))
|
||||||
p.setX(int(self.size().width()-br.height()+nudge))
|
p.setX(int(self.size().width()-br.height()+nudge))
|
||||||
elif self.orientation == 'top':
|
elif self.orientation == 'top':
|
||||||
#s.setHeight(10)
|
|
||||||
p.setY(-nudge)
|
p.setY(-nudge)
|
||||||
p.setX(int(self.size().width()/2. - br.width()/2.))
|
p.setX(int(self.size().width()/2. - br.width()/2.))
|
||||||
elif self.orientation == 'bottom':
|
elif self.orientation == 'bottom':
|
||||||
p.setX(int(self.size().width()/2. - br.width()/2.))
|
p.setX(int(self.size().width()/2. - br.width()/2.))
|
||||||
#s.setHeight(10)
|
|
||||||
p.setY(int(self.size().height()-br.height()+nudge))
|
p.setY(int(self.size().height()-br.height()+nudge))
|
||||||
#self.label.resize(s)
|
|
||||||
self.label.setPos(p)
|
self.label.setPos(p)
|
||||||
self.picture = None
|
self.picture = None
|
||||||
|
|
||||||
@ -156,17 +221,17 @@ class AxisItem(GraphicsWidget):
|
|||||||
def setLabel(self, text=None, units=None, unitPrefix=None, **args):
|
def setLabel(self, text=None, units=None, unitPrefix=None, **args):
|
||||||
"""Set the text displayed adjacent to the axis.
|
"""Set the text displayed adjacent to the axis.
|
||||||
|
|
||||||
============= =============================================================
|
============== =============================================================
|
||||||
Arguments
|
**Arguments:**
|
||||||
text The text (excluding units) to display on the label for this
|
text The text (excluding units) to display on the label for this
|
||||||
axis.
|
axis.
|
||||||
units The units for this axis. Units should generally be given
|
units The units for this axis. Units should generally be given
|
||||||
without any scaling prefix (eg, 'V' instead of 'mV'). The
|
without any scaling prefix (eg, 'V' instead of 'mV'). The
|
||||||
scaling prefix will be automatically prepended based on the
|
scaling prefix will be automatically prepended based on the
|
||||||
range of data displayed.
|
range of data displayed.
|
||||||
**args All extra keyword arguments become CSS style options for
|
**args All extra keyword arguments become CSS style options for
|
||||||
the <span> tag which will surround the axis label and units.
|
the <span> tag which will surround the axis label and units.
|
||||||
============= =============================================================
|
============== =============================================================
|
||||||
|
|
||||||
The final text generated for the label will look like::
|
The final text generated for the label will look like::
|
||||||
|
|
||||||
@ -239,27 +304,32 @@ class AxisItem(GraphicsWidget):
|
|||||||
"""Set the height of this axis reserved for ticks and tick labels.
|
"""Set the height of this axis reserved for ticks and tick labels.
|
||||||
The height of the axis label is automatically added."""
|
The height of the axis label is automatically added."""
|
||||||
if h is None:
|
if h is None:
|
||||||
if self.style['autoExpandTextSpace'] is True:
|
if not self.style['showValues']:
|
||||||
|
h = 0
|
||||||
|
elif self.style['autoExpandTextSpace'] is True:
|
||||||
h = self.textHeight
|
h = self.textHeight
|
||||||
else:
|
else:
|
||||||
h = self.style['tickTextHeight']
|
h = self.style['tickTextHeight']
|
||||||
h += max(0, self.tickLength) + self.style['tickTextOffset'][1]
|
h += self.style['tickTextOffset'][1] if self.style['showValues'] else 0
|
||||||
|
h += max(0, self.style['tickLength'])
|
||||||
if self.label.isVisible():
|
if self.label.isVisible():
|
||||||
h += self.label.boundingRect().height() * 0.8
|
h += self.label.boundingRect().height() * 0.8
|
||||||
self.setMaximumHeight(h)
|
self.setMaximumHeight(h)
|
||||||
self.setMinimumHeight(h)
|
self.setMinimumHeight(h)
|
||||||
self.picture = None
|
self.picture = None
|
||||||
|
|
||||||
|
|
||||||
def setWidth(self, w=None):
|
def setWidth(self, w=None):
|
||||||
"""Set the width of this axis reserved for ticks and tick labels.
|
"""Set the width of this axis reserved for ticks and tick labels.
|
||||||
The width of the axis label is automatically added."""
|
The width of the axis label is automatically added."""
|
||||||
if w is None:
|
if w is None:
|
||||||
if self.style['autoExpandTextSpace'] is True:
|
if not self.style['showValues']:
|
||||||
|
w = 0
|
||||||
|
elif self.style['autoExpandTextSpace'] is True:
|
||||||
w = self.textWidth
|
w = self.textWidth
|
||||||
else:
|
else:
|
||||||
w = self.style['tickTextWidth']
|
w = self.style['tickTextWidth']
|
||||||
w += max(0, self.tickLength) + self.style['tickTextOffset'][0]
|
w += self.style['tickTextOffset'][0] if self.style['showValues'] else 0
|
||||||
|
w += max(0, self.style['tickLength'])
|
||||||
if self.label.isVisible():
|
if self.label.isVisible():
|
||||||
w += self.label.boundingRect().height() * 0.8 ## bounding rect is usually an overestimate
|
w += self.label.boundingRect().height() * 0.8 ## bounding rect is usually an overestimate
|
||||||
self.setMaximumWidth(w)
|
self.setMaximumWidth(w)
|
||||||
@ -271,16 +341,17 @@ class AxisItem(GraphicsWidget):
|
|||||||
return fn.mkPen(getConfigOption('foreground'))
|
return fn.mkPen(getConfigOption('foreground'))
|
||||||
return fn.mkPen(self._pen)
|
return fn.mkPen(self._pen)
|
||||||
|
|
||||||
def setPen(self, pen):
|
def setPen(self, *args, **kwargs):
|
||||||
"""
|
"""
|
||||||
Set the pen used for drawing text, axes, ticks, and grid lines.
|
Set the pen used for drawing text, axes, ticks, and grid lines.
|
||||||
if pen == None, the default will be used (see :func:`setConfigOption
|
If no arguments are given, the default foreground color will be used
|
||||||
<pyqtgraph.setConfigOption>`)
|
(see :func:`setConfigOption <pyqtgraph.setConfigOption>`).
|
||||||
"""
|
"""
|
||||||
self.picture = None
|
self.picture = None
|
||||||
if pen is None:
|
if args or kwargs:
|
||||||
pen = getConfigOption('foreground')
|
self._pen = fn.mkPen(*args, **kwargs)
|
||||||
self._pen = fn.mkPen(pen)
|
else:
|
||||||
|
self._pen = fn.mkPen(getConfigOption('foreground'))
|
||||||
self.labelStyle['color'] = '#' + fn.colorStr(self._pen.color())[:6]
|
self.labelStyle['color'] = '#' + fn.colorStr(self._pen.color())[:6]
|
||||||
self.setLabel()
|
self.setLabel()
|
||||||
self.update()
|
self.update()
|
||||||
@ -391,14 +462,15 @@ class AxisItem(GraphicsWidget):
|
|||||||
rect = self.mapRectFromParent(self.geometry())
|
rect = self.mapRectFromParent(self.geometry())
|
||||||
## extend rect if ticks go in negative direction
|
## extend rect if ticks go in negative direction
|
||||||
## also extend to account for text that flows past the edges
|
## also extend to account for text that flows past the edges
|
||||||
|
tl = self.style['tickLength']
|
||||||
if self.orientation == 'left':
|
if self.orientation == 'left':
|
||||||
rect = rect.adjusted(0, -15, -min(0,self.tickLength), 15)
|
rect = rect.adjusted(0, -15, -min(0,tl), 15)
|
||||||
elif self.orientation == 'right':
|
elif self.orientation == 'right':
|
||||||
rect = rect.adjusted(min(0,self.tickLength), -15, 0, 15)
|
rect = rect.adjusted(min(0,tl), -15, 0, 15)
|
||||||
elif self.orientation == 'top':
|
elif self.orientation == 'top':
|
||||||
rect = rect.adjusted(-15, 0, 15, -min(0,self.tickLength))
|
rect = rect.adjusted(-15, 0, 15, -min(0,tl))
|
||||||
elif self.orientation == 'bottom':
|
elif self.orientation == 'bottom':
|
||||||
rect = rect.adjusted(-15, min(0,self.tickLength), 15, 0)
|
rect = rect.adjusted(-15, min(0,tl), 15, 0)
|
||||||
return rect
|
return rect
|
||||||
else:
|
else:
|
||||||
return self.mapRectFromParent(self.geometry()) | linkedView.mapRectToItem(self, linkedView.boundingRect())
|
return self.mapRectFromParent(self.geometry()) | linkedView.mapRectToItem(self, linkedView.boundingRect())
|
||||||
@ -618,7 +690,7 @@ class AxisItem(GraphicsWidget):
|
|||||||
|
|
||||||
def generateDrawSpecs(self, p):
|
def generateDrawSpecs(self, p):
|
||||||
"""
|
"""
|
||||||
Calls tickValues() and tickStrings to determine where and how ticks should
|
Calls tickValues() and tickStrings() to determine where and how ticks should
|
||||||
be drawn, then generates from this a set of drawing commands to be
|
be drawn, then generates from this a set of drawing commands to be
|
||||||
interpreted by drawPicture().
|
interpreted by drawPicture().
|
||||||
"""
|
"""
|
||||||
@ -667,6 +739,7 @@ class AxisItem(GraphicsWidget):
|
|||||||
if lengthInPixels == 0:
|
if lengthInPixels == 0:
|
||||||
return
|
return
|
||||||
|
|
||||||
|
# Determine major / minor / subminor axis ticks
|
||||||
if self._tickLevels is None:
|
if self._tickLevels is None:
|
||||||
tickLevels = self.tickValues(self.range[0], self.range[1], lengthInPixels)
|
tickLevels = self.tickValues(self.range[0], self.range[1], lengthInPixels)
|
||||||
tickStrings = None
|
tickStrings = None
|
||||||
@ -688,7 +761,7 @@ class AxisItem(GraphicsWidget):
|
|||||||
## determine mapping between tick values and local coordinates
|
## determine mapping between tick values and local coordinates
|
||||||
dif = self.range[1] - self.range[0]
|
dif = self.range[1] - self.range[0]
|
||||||
if dif == 0:
|
if dif == 0:
|
||||||
xscale = 1
|
xScale = 1
|
||||||
offset = 0
|
offset = 0
|
||||||
else:
|
else:
|
||||||
if axis == 0:
|
if axis == 0:
|
||||||
@ -706,8 +779,7 @@ class AxisItem(GraphicsWidget):
|
|||||||
|
|
||||||
tickPositions = [] # remembers positions of previously drawn ticks
|
tickPositions = [] # remembers positions of previously drawn ticks
|
||||||
|
|
||||||
## draw ticks
|
## compute coordinates to draw ticks
|
||||||
## (to improve performance, we do not interleave line and text drawing, since this causes unnecessary pipeline switching)
|
|
||||||
## draw three different intervals, long ticks first
|
## draw three different intervals, long ticks first
|
||||||
tickSpecs = []
|
tickSpecs = []
|
||||||
for i in range(len(tickLevels)):
|
for i in range(len(tickLevels)):
|
||||||
@ -715,7 +787,7 @@ class AxisItem(GraphicsWidget):
|
|||||||
ticks = tickLevels[i][1]
|
ticks = tickLevels[i][1]
|
||||||
|
|
||||||
## length of tick
|
## length of tick
|
||||||
tickLength = self.tickLength / ((i*0.5)+1.0)
|
tickLength = self.style['tickLength'] / ((i*0.5)+1.0)
|
||||||
|
|
||||||
lineAlpha = 255 / (i+1)
|
lineAlpha = 255 / (i+1)
|
||||||
if self.grid is not False:
|
if self.grid is not False:
|
||||||
@ -742,7 +814,6 @@ class AxisItem(GraphicsWidget):
|
|||||||
tickSpecs.append((tickPen, Point(p1), Point(p2)))
|
tickSpecs.append((tickPen, Point(p1), Point(p2)))
|
||||||
profiler('compute ticks')
|
profiler('compute ticks')
|
||||||
|
|
||||||
## This is where the long axis line should be drawn
|
|
||||||
|
|
||||||
if self.style['stopAxisAtTick'][0] is True:
|
if self.style['stopAxisAtTick'][0] is True:
|
||||||
stop = max(span[0].y(), min(map(min, tickPositions)))
|
stop = max(span[0].y(), min(map(min, tickPositions)))
|
||||||
@ -759,7 +830,6 @@ class AxisItem(GraphicsWidget):
|
|||||||
axisSpec = (self.pen(), span[0], span[1])
|
axisSpec = (self.pen(), span[0], span[1])
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
textOffset = self.style['tickTextOffset'][axis] ## spacing between axis and text
|
textOffset = self.style['tickTextOffset'][axis] ## spacing between axis and text
|
||||||
#if self.style['autoExpandTextSpace'] is True:
|
#if self.style['autoExpandTextSpace'] is True:
|
||||||
#textWidth = self.textWidth
|
#textWidth = self.textWidth
|
||||||
@ -771,7 +841,11 @@ class AxisItem(GraphicsWidget):
|
|||||||
textSize2 = 0
|
textSize2 = 0
|
||||||
textRects = []
|
textRects = []
|
||||||
textSpecs = [] ## list of draw
|
textSpecs = [] ## list of draw
|
||||||
textSize2 = 0
|
|
||||||
|
# If values are hidden, return early
|
||||||
|
if not self.style['showValues']:
|
||||||
|
return (axisSpec, tickSpecs, textSpecs)
|
||||||
|
|
||||||
for i in range(len(tickLevels)):
|
for i in range(len(tickLevels)):
|
||||||
## Get the list of strings to display for this level
|
## Get the list of strings to display for this level
|
||||||
if tickStrings is None:
|
if tickStrings is None:
|
||||||
@ -802,15 +876,15 @@ class AxisItem(GraphicsWidget):
|
|||||||
rects.append(br)
|
rects.append(br)
|
||||||
textRects.append(rects[-1])
|
textRects.append(rects[-1])
|
||||||
|
|
||||||
if i > 0: ## always draw top level
|
## measure all text, make sure there's enough room
|
||||||
## measure all text, make sure there's enough room
|
if axis == 0:
|
||||||
if axis == 0:
|
textSize = np.sum([r.height() for r in textRects])
|
||||||
textSize = np.sum([r.height() for r in textRects])
|
textSize2 = np.max([r.width() for r in textRects]) if textRects else 0
|
||||||
textSize2 = np.max([r.width() for r in textRects])
|
else:
|
||||||
else:
|
textSize = np.sum([r.width() for r in textRects])
|
||||||
textSize = np.sum([r.width() for r in textRects])
|
textSize2 = np.max([r.height() for r in textRects]) if textRects else 0
|
||||||
textSize2 = np.max([r.height() for r in textRects])
|
|
||||||
|
|
||||||
|
if i > 0: ## always draw top level
|
||||||
## If the strings are too crowded, stop drawing text now.
|
## If the strings are too crowded, stop drawing text now.
|
||||||
## We use three different crowding limits based on the number
|
## We use three different crowding limits based on the number
|
||||||
## of texts drawn so far.
|
## of texts drawn so far.
|
||||||
@ -825,6 +899,7 @@ class AxisItem(GraphicsWidget):
|
|||||||
|
|
||||||
#spacing, values = tickLevels[best]
|
#spacing, values = tickLevels[best]
|
||||||
#strings = self.tickStrings(values, self.scale, spacing)
|
#strings = self.tickStrings(values, self.scale, spacing)
|
||||||
|
# Determine exactly where tick text should be drawn
|
||||||
for j in range(len(strings)):
|
for j in range(len(strings)):
|
||||||
vstr = strings[j]
|
vstr = strings[j]
|
||||||
if vstr is None: ## this tick was ignored because it is out of bounds
|
if vstr is None: ## this tick was ignored because it is out of bounds
|
||||||
@ -836,7 +911,7 @@ class AxisItem(GraphicsWidget):
|
|||||||
height = textRect.height()
|
height = textRect.height()
|
||||||
width = textRect.width()
|
width = textRect.width()
|
||||||
#self.textHeight = height
|
#self.textHeight = height
|
||||||
offset = max(0,self.tickLength) + textOffset
|
offset = max(0,self.style['tickLength']) + textOffset
|
||||||
if self.orientation == 'left':
|
if self.orientation == 'left':
|
||||||
textFlags = QtCore.Qt.TextDontClip|QtCore.Qt.AlignRight|QtCore.Qt.AlignVCenter
|
textFlags = QtCore.Qt.TextDontClip|QtCore.Qt.AlignRight|QtCore.Qt.AlignVCenter
|
||||||
rect = QtCore.QRectF(tickStop-offset-width, x-(height/2), width, height)
|
rect = QtCore.QRectF(tickStop-offset-width, x-(height/2), width, height)
|
||||||
@ -854,7 +929,7 @@ class AxisItem(GraphicsWidget):
|
|||||||
#p.drawText(rect, textFlags, vstr)
|
#p.drawText(rect, textFlags, vstr)
|
||||||
textSpecs.append((rect, textFlags, vstr))
|
textSpecs.append((rect, textFlags, vstr))
|
||||||
profiler('compute text')
|
profiler('compute text')
|
||||||
|
|
||||||
## update max text size if needed.
|
## update max text size if needed.
|
||||||
self._updateMaxTextSize(textSize2)
|
self._updateMaxTextSize(textSize2)
|
||||||
|
|
||||||
|
@ -47,16 +47,20 @@ class BarGraphItem(GraphicsObject):
|
|||||||
pens=None,
|
pens=None,
|
||||||
brushes=None,
|
brushes=None,
|
||||||
)
|
)
|
||||||
|
self._shape = None
|
||||||
|
self.picture = None
|
||||||
self.setOpts(**opts)
|
self.setOpts(**opts)
|
||||||
|
|
||||||
def setOpts(self, **opts):
|
def setOpts(self, **opts):
|
||||||
self.opts.update(opts)
|
self.opts.update(opts)
|
||||||
self.picture = None
|
self.picture = None
|
||||||
|
self._shape = None
|
||||||
self.update()
|
self.update()
|
||||||
self.informViewBoundsChanged()
|
self.informViewBoundsChanged()
|
||||||
|
|
||||||
def drawPicture(self):
|
def drawPicture(self):
|
||||||
self.picture = QtGui.QPicture()
|
self.picture = QtGui.QPicture()
|
||||||
|
self._shape = QtGui.QPainterPath()
|
||||||
p = QtGui.QPainter(self.picture)
|
p = QtGui.QPainter(self.picture)
|
||||||
|
|
||||||
pen = self.opts['pen']
|
pen = self.opts['pen']
|
||||||
@ -122,6 +126,10 @@ class BarGraphItem(GraphicsObject):
|
|||||||
if brushes is not None:
|
if brushes is not None:
|
||||||
p.setBrush(fn.mkBrush(brushes[i]))
|
p.setBrush(fn.mkBrush(brushes[i]))
|
||||||
|
|
||||||
|
if np.isscalar(x0):
|
||||||
|
x = x0
|
||||||
|
else:
|
||||||
|
x = x0[i]
|
||||||
if np.isscalar(y0):
|
if np.isscalar(y0):
|
||||||
y = y0
|
y = y0
|
||||||
else:
|
else:
|
||||||
@ -130,9 +138,15 @@ class BarGraphItem(GraphicsObject):
|
|||||||
w = width
|
w = width
|
||||||
else:
|
else:
|
||||||
w = width[i]
|
w = width[i]
|
||||||
|
if np.isscalar(height):
|
||||||
p.drawRect(QtCore.QRectF(x0[i], y, w, height[i]))
|
h = height
|
||||||
|
else:
|
||||||
|
h = height[i]
|
||||||
|
|
||||||
|
|
||||||
|
rect = QtCore.QRectF(x, y, w, h)
|
||||||
|
p.drawRect(rect)
|
||||||
|
self._shape.addRect(rect)
|
||||||
|
|
||||||
p.end()
|
p.end()
|
||||||
self.prepareGeometryChange()
|
self.prepareGeometryChange()
|
||||||
@ -148,4 +162,7 @@ class BarGraphItem(GraphicsObject):
|
|||||||
self.drawPicture()
|
self.drawPicture()
|
||||||
return QtCore.QRectF(self.picture.boundingRect())
|
return QtCore.QRectF(self.picture.boundingRect())
|
||||||
|
|
||||||
|
def shape(self):
|
||||||
|
if self.picture is None:
|
||||||
|
self.drawPicture()
|
||||||
|
return self._shape
|
||||||
|
@ -112,6 +112,6 @@ class CurveArrow(CurvePoint):
|
|||||||
self.arrow = ArrowItem.ArrowItem(**opts)
|
self.arrow = ArrowItem.ArrowItem(**opts)
|
||||||
self.arrow.setParentItem(self)
|
self.arrow.setParentItem(self)
|
||||||
|
|
||||||
def setStyle(**opts):
|
def setStyle(self, **opts):
|
||||||
return self.arrow.setStyle(**opts)
|
return self.arrow.setStyle(**opts)
|
||||||
|
|
||||||
|
@ -22,13 +22,16 @@ class FillBetweenItem(QtGui.QGraphicsPathItem):
|
|||||||
def setCurves(self, curve1, curve2):
|
def setCurves(self, curve1, curve2):
|
||||||
"""Set the curves to fill between.
|
"""Set the curves to fill between.
|
||||||
|
|
||||||
Arguments must be instances of PlotDataItem or PlotCurveItem."""
|
Arguments must be instances of PlotDataItem or PlotCurveItem.
|
||||||
|
|
||||||
|
Added in version 0.9.9
|
||||||
|
"""
|
||||||
|
|
||||||
if self.curves is not None:
|
if self.curves is not None:
|
||||||
for c in self.curves:
|
for c in self.curves:
|
||||||
try:
|
try:
|
||||||
c.sigPlotChanged.disconnect(self.curveChanged)
|
c.sigPlotChanged.disconnect(self.curveChanged)
|
||||||
except TypeError:
|
except (TypeError, RuntimeError):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
curves = [curve1, curve2]
|
curves = [curve1, curve2]
|
||||||
|
@ -35,14 +35,14 @@ class TickSliderItem(GraphicsWidget):
|
|||||||
|
|
||||||
def __init__(self, orientation='bottom', allowAdd=True, **kargs):
|
def __init__(self, orientation='bottom', allowAdd=True, **kargs):
|
||||||
"""
|
"""
|
||||||
============= =================================================================================
|
============== =================================================================================
|
||||||
**Arguments**
|
**Arguments:**
|
||||||
orientation Set the orientation of the gradient. Options are: 'left', 'right'
|
orientation Set the orientation of the gradient. Options are: 'left', 'right'
|
||||||
'top', and 'bottom'.
|
'top', and 'bottom'.
|
||||||
allowAdd Specifies whether ticks can be added to the item by the user.
|
allowAdd Specifies whether ticks can be added to the item by the user.
|
||||||
tickPen Default is white. Specifies the color of the outline of the ticks.
|
tickPen Default is white. Specifies the color of the outline of the ticks.
|
||||||
Can be any of the valid arguments for :func:`mkPen <pyqtgraph.mkPen>`
|
Can be any of the valid arguments for :func:`mkPen <pyqtgraph.mkPen>`
|
||||||
============= =================================================================================
|
============== =================================================================================
|
||||||
"""
|
"""
|
||||||
## public
|
## public
|
||||||
GraphicsWidget.__init__(self)
|
GraphicsWidget.__init__(self)
|
||||||
@ -103,13 +103,13 @@ class TickSliderItem(GraphicsWidget):
|
|||||||
## public
|
## public
|
||||||
"""Set the orientation of the TickSliderItem.
|
"""Set the orientation of the TickSliderItem.
|
||||||
|
|
||||||
============= ===================================================================
|
============== ===================================================================
|
||||||
**Arguments**
|
**Arguments:**
|
||||||
orientation Options are: 'left', 'right', 'top', 'bottom'
|
orientation Options are: 'left', 'right', 'top', 'bottom'
|
||||||
The orientation option specifies which side of the slider the
|
The orientation option specifies which side of the slider the
|
||||||
ticks are on, as well as whether the slider is vertical ('right'
|
ticks are on, as well as whether the slider is vertical ('right'
|
||||||
and 'left') or horizontal ('top' and 'bottom').
|
and 'left') or horizontal ('top' and 'bottom').
|
||||||
============= ===================================================================
|
============== ===================================================================
|
||||||
"""
|
"""
|
||||||
self.orientation = orientation
|
self.orientation = orientation
|
||||||
self.setMaxDim()
|
self.setMaxDim()
|
||||||
@ -136,13 +136,13 @@ class TickSliderItem(GraphicsWidget):
|
|||||||
"""
|
"""
|
||||||
Add a tick to the item.
|
Add a tick to the item.
|
||||||
|
|
||||||
============= ==================================================================
|
============== ==================================================================
|
||||||
**Arguments**
|
**Arguments:**
|
||||||
x Position where tick should be added.
|
x Position where tick should be added.
|
||||||
color Color of added tick. If color is not specified, the color will be
|
color Color of added tick. If color is not specified, the color will be
|
||||||
white.
|
white.
|
||||||
movable Specifies whether the tick is movable with the mouse.
|
movable Specifies whether the tick is movable with the mouse.
|
||||||
============= ==================================================================
|
============== ==================================================================
|
||||||
"""
|
"""
|
||||||
|
|
||||||
if color is None:
|
if color is None:
|
||||||
@ -265,14 +265,14 @@ class TickSliderItem(GraphicsWidget):
|
|||||||
def setTickColor(self, tick, color):
|
def setTickColor(self, tick, color):
|
||||||
"""Set the color of the specified tick.
|
"""Set the color of the specified tick.
|
||||||
|
|
||||||
============= ==================================================================
|
============== ==================================================================
|
||||||
**Arguments**
|
**Arguments:**
|
||||||
tick Can be either an integer corresponding to the index of the tick
|
tick Can be either an integer corresponding to the index of the tick
|
||||||
or a Tick object. Ex: if you had a slider with 3 ticks and you
|
or a Tick object. Ex: if you had a slider with 3 ticks and you
|
||||||
wanted to change the middle tick, the index would be 1.
|
wanted to change the middle tick, the index would be 1.
|
||||||
color The color to make the tick. Can be any argument that is valid for
|
color The color to make the tick. Can be any argument that is valid for
|
||||||
:func:`mkBrush <pyqtgraph.mkBrush>`
|
:func:`mkBrush <pyqtgraph.mkBrush>`
|
||||||
============= ==================================================================
|
============== ==================================================================
|
||||||
"""
|
"""
|
||||||
tick = self.getTick(tick)
|
tick = self.getTick(tick)
|
||||||
tick.color = color
|
tick.color = color
|
||||||
@ -284,14 +284,14 @@ class TickSliderItem(GraphicsWidget):
|
|||||||
"""
|
"""
|
||||||
Set the position (along the slider) of the tick.
|
Set the position (along the slider) of the tick.
|
||||||
|
|
||||||
============= ==================================================================
|
============== ==================================================================
|
||||||
**Arguments**
|
**Arguments:**
|
||||||
tick Can be either an integer corresponding to the index of the tick
|
tick Can be either an integer corresponding to the index of the tick
|
||||||
or a Tick object. Ex: if you had a slider with 3 ticks and you
|
or a Tick object. Ex: if you had a slider with 3 ticks and you
|
||||||
wanted to change the middle tick, the index would be 1.
|
wanted to change the middle tick, the index would be 1.
|
||||||
val The desired position of the tick. If val is < 0, position will be
|
val The desired position of the tick. If val is < 0, position will be
|
||||||
set to 0. If val is > 1, position will be set to 1.
|
set to 0. If val is > 1, position will be set to 1.
|
||||||
============= ==================================================================
|
============== ==================================================================
|
||||||
"""
|
"""
|
||||||
tick = self.getTick(tick)
|
tick = self.getTick(tick)
|
||||||
val = min(max(0.0, val), 1.0)
|
val = min(max(0.0, val), 1.0)
|
||||||
@ -305,12 +305,12 @@ class TickSliderItem(GraphicsWidget):
|
|||||||
## public
|
## public
|
||||||
"""Return the value (from 0.0 to 1.0) of the specified tick.
|
"""Return the value (from 0.0 to 1.0) of the specified tick.
|
||||||
|
|
||||||
============= ==================================================================
|
============== ==================================================================
|
||||||
**Arguments**
|
**Arguments:**
|
||||||
tick Can be either an integer corresponding to the index of the tick
|
tick Can be either an integer corresponding to the index of the tick
|
||||||
or a Tick object. Ex: if you had a slider with 3 ticks and you
|
or a Tick object. Ex: if you had a slider with 3 ticks and you
|
||||||
wanted the value of the middle tick, the index would be 1.
|
wanted the value of the middle tick, the index would be 1.
|
||||||
============= ==================================================================
|
============== ==================================================================
|
||||||
"""
|
"""
|
||||||
tick = self.getTick(tick)
|
tick = self.getTick(tick)
|
||||||
return self.ticks[tick]
|
return self.ticks[tick]
|
||||||
@ -319,11 +319,11 @@ class TickSliderItem(GraphicsWidget):
|
|||||||
## public
|
## public
|
||||||
"""Return the Tick object at the specified index.
|
"""Return the Tick object at the specified index.
|
||||||
|
|
||||||
============= ==================================================================
|
============== ==================================================================
|
||||||
**Arguments**
|
**Arguments:**
|
||||||
tick An integer corresponding to the index of the desired tick. If the
|
tick An integer corresponding to the index of the desired tick. If the
|
||||||
argument is not an integer it will be returned unchanged.
|
argument is not an integer it will be returned unchanged.
|
||||||
============= ==================================================================
|
============== ==================================================================
|
||||||
"""
|
"""
|
||||||
if type(tick) is int:
|
if type(tick) is int:
|
||||||
tick = self.listTicks()[tick][0]
|
tick = self.listTicks()[tick][0]
|
||||||
@ -349,7 +349,7 @@ class GradientEditorItem(TickSliderItem):
|
|||||||
with a GradientEditorItem that can be added to a GUI.
|
with a GradientEditorItem that can be added to a GUI.
|
||||||
|
|
||||||
================================ ===========================================================
|
================================ ===========================================================
|
||||||
**Signals**
|
**Signals:**
|
||||||
sigGradientChanged(self) Signal is emitted anytime the gradient changes. The signal
|
sigGradientChanged(self) Signal is emitted anytime the gradient changes. The signal
|
||||||
is emitted in real time while ticks are being dragged or
|
is emitted in real time while ticks are being dragged or
|
||||||
colors are being changed.
|
colors are being changed.
|
||||||
@ -366,14 +366,14 @@ class GradientEditorItem(TickSliderItem):
|
|||||||
Create a new GradientEditorItem.
|
Create a new GradientEditorItem.
|
||||||
All arguments are passed to :func:`TickSliderItem.__init__ <pyqtgraph.TickSliderItem.__init__>`
|
All arguments are passed to :func:`TickSliderItem.__init__ <pyqtgraph.TickSliderItem.__init__>`
|
||||||
|
|
||||||
============= =================================================================================
|
=============== =================================================================================
|
||||||
**Arguments**
|
**Arguments:**
|
||||||
orientation Set the orientation of the gradient. Options are: 'left', 'right'
|
orientation Set the orientation of the gradient. Options are: 'left', 'right'
|
||||||
'top', and 'bottom'.
|
'top', and 'bottom'.
|
||||||
allowAdd Default is True. Specifies whether ticks can be added to the item.
|
allowAdd Default is True. Specifies whether ticks can be added to the item.
|
||||||
tickPen Default is white. Specifies the color of the outline of the ticks.
|
tickPen Default is white. Specifies the color of the outline of the ticks.
|
||||||
Can be any of the valid arguments for :func:`mkPen <pyqtgraph.mkPen>`
|
Can be any of the valid arguments for :func:`mkPen <pyqtgraph.mkPen>`
|
||||||
============= =================================================================================
|
=============== =================================================================================
|
||||||
"""
|
"""
|
||||||
self.currentTick = None
|
self.currentTick = None
|
||||||
self.currentTickColor = None
|
self.currentTickColor = None
|
||||||
@ -445,13 +445,13 @@ class GradientEditorItem(TickSliderItem):
|
|||||||
"""
|
"""
|
||||||
Set the orientation of the GradientEditorItem.
|
Set the orientation of the GradientEditorItem.
|
||||||
|
|
||||||
============= ===================================================================
|
============== ===================================================================
|
||||||
**Arguments**
|
**Arguments:**
|
||||||
orientation Options are: 'left', 'right', 'top', 'bottom'
|
orientation Options are: 'left', 'right', 'top', 'bottom'
|
||||||
The orientation option specifies which side of the gradient the
|
The orientation option specifies which side of the gradient the
|
||||||
ticks are on, as well as whether the gradient is vertical ('right'
|
ticks are on, as well as whether the gradient is vertical ('right'
|
||||||
and 'left') or horizontal ('top' and 'bottom').
|
and 'left') or horizontal ('top' and 'bottom').
|
||||||
============= ===================================================================
|
============== ===================================================================
|
||||||
"""
|
"""
|
||||||
TickSliderItem.setOrientation(self, orientation)
|
TickSliderItem.setOrientation(self, orientation)
|
||||||
self.translate(0, self.rectSize)
|
self.translate(0, self.rectSize)
|
||||||
@ -588,11 +588,11 @@ class GradientEditorItem(TickSliderItem):
|
|||||||
"""
|
"""
|
||||||
Return a color for a given value.
|
Return a color for a given value.
|
||||||
|
|
||||||
============= ==================================================================
|
============== ==================================================================
|
||||||
**Arguments**
|
**Arguments:**
|
||||||
x Value (position on gradient) of requested color.
|
x Value (position on gradient) of requested color.
|
||||||
toQColor If true, returns a QColor object, else returns a (r,g,b,a) tuple.
|
toQColor If true, returns a QColor object, else returns a (r,g,b,a) tuple.
|
||||||
============= ==================================================================
|
============== ==================================================================
|
||||||
"""
|
"""
|
||||||
ticks = self.listTicks()
|
ticks = self.listTicks()
|
||||||
if x <= ticks[0][1]:
|
if x <= ticks[0][1]:
|
||||||
@ -648,12 +648,12 @@ class GradientEditorItem(TickSliderItem):
|
|||||||
"""
|
"""
|
||||||
Return an RGB(A) lookup table (ndarray).
|
Return an RGB(A) lookup table (ndarray).
|
||||||
|
|
||||||
============= ============================================================================
|
============== ============================================================================
|
||||||
**Arguments**
|
**Arguments:**
|
||||||
nPts The number of points in the returned lookup table.
|
nPts The number of points in the returned lookup table.
|
||||||
alpha True, False, or None - Specifies whether or not alpha values are included
|
alpha True, False, or None - Specifies whether or not alpha values are included
|
||||||
in the table.If alpha is None, alpha will be automatically determined.
|
in the table.If alpha is None, alpha will be automatically determined.
|
||||||
============= ============================================================================
|
============== ============================================================================
|
||||||
"""
|
"""
|
||||||
if alpha is None:
|
if alpha is None:
|
||||||
alpha = self.usesAlpha()
|
alpha = self.usesAlpha()
|
||||||
@ -702,13 +702,13 @@ class GradientEditorItem(TickSliderItem):
|
|||||||
"""
|
"""
|
||||||
Add a tick to the gradient. Return the tick.
|
Add a tick to the gradient. Return the tick.
|
||||||
|
|
||||||
============= ==================================================================
|
============== ==================================================================
|
||||||
**Arguments**
|
**Arguments:**
|
||||||
x Position where tick should be added.
|
x Position where tick should be added.
|
||||||
color Color of added tick. If color is not specified, the color will be
|
color Color of added tick. If color is not specified, the color will be
|
||||||
the color of the gradient at the specified position.
|
the color of the gradient at the specified position.
|
||||||
movable Specifies whether the tick is movable with the mouse.
|
movable Specifies whether the tick is movable with the mouse.
|
||||||
============= ==================================================================
|
============== ==================================================================
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
@ -748,16 +748,16 @@ class GradientEditorItem(TickSliderItem):
|
|||||||
"""
|
"""
|
||||||
Restore the gradient specified in state.
|
Restore the gradient specified in state.
|
||||||
|
|
||||||
============= ====================================================================
|
============== ====================================================================
|
||||||
**Arguments**
|
**Arguments:**
|
||||||
state A dictionary with same structure as those returned by
|
state A dictionary with same structure as those returned by
|
||||||
:func:`saveState <pyqtgraph.GradientEditorItem.saveState>`
|
:func:`saveState <pyqtgraph.GradientEditorItem.saveState>`
|
||||||
|
|
||||||
Keys must include:
|
Keys must include:
|
||||||
|
|
||||||
- 'mode': hsv or rgb
|
- 'mode': hsv or rgb
|
||||||
- 'ticks': a list of tuples (pos, (r,g,b,a))
|
- 'ticks': a list of tuples (pos, (r,g,b,a))
|
||||||
============= ====================================================================
|
============== ====================================================================
|
||||||
"""
|
"""
|
||||||
## public
|
## public
|
||||||
self.setColorMode(state['mode'])
|
self.setColorMode(state['mode'])
|
||||||
|
@ -28,48 +28,72 @@ class GraphItem(GraphicsObject):
|
|||||||
"""
|
"""
|
||||||
Change the data displayed by the graph.
|
Change the data displayed by the graph.
|
||||||
|
|
||||||
============ =========================================================
|
============== =======================================================================
|
||||||
Arguments
|
**Arguments:**
|
||||||
pos (N,2) array of the positions of each node in the graph.
|
pos (N,2) array of the positions of each node in the graph.
|
||||||
adj (M,2) array of connection data. Each row contains indexes
|
adj (M,2) array of connection data. Each row contains indexes
|
||||||
of two nodes that are connected.
|
of two nodes that are connected.
|
||||||
pen The pen to use when drawing lines between connected
|
pen The pen to use when drawing lines between connected
|
||||||
nodes. May be one of:
|
nodes. May be one of:
|
||||||
|
|
||||||
* QPen
|
* QPen
|
||||||
* a single argument to pass to pg.mkPen
|
* a single argument to pass to pg.mkPen
|
||||||
* a record array of length M
|
* a record array of length M
|
||||||
with fields (red, green, blue, alpha, width). Note
|
with fields (red, green, blue, alpha, width). Note
|
||||||
that using this option may have a significant performance
|
that using this option may have a significant performance
|
||||||
cost.
|
cost.
|
||||||
* None (to disable connection drawing)
|
* None (to disable connection drawing)
|
||||||
* 'default' to use the default foreground color.
|
* 'default' to use the default foreground color.
|
||||||
|
|
||||||
symbolPen The pen used for drawing nodes.
|
symbolPen The pen(s) used for drawing nodes.
|
||||||
``**opts`` All other keyword arguments are given to
|
symbolBrush The brush(es) used for drawing nodes.
|
||||||
:func:`ScatterPlotItem.setData() <pyqtgraph.ScatterPlotItem.setData>`
|
``**opts`` All other keyword arguments are given to
|
||||||
to affect the appearance of nodes (symbol, size, brush,
|
:func:`ScatterPlotItem.setData() <pyqtgraph.ScatterPlotItem.setData>`
|
||||||
etc.)
|
to affect the appearance of nodes (symbol, size, brush,
|
||||||
============ =========================================================
|
etc.)
|
||||||
|
============== =======================================================================
|
||||||
"""
|
"""
|
||||||
if 'adj' in kwds:
|
if 'adj' in kwds:
|
||||||
self.adjacency = kwds.pop('adj')
|
self.adjacency = kwds.pop('adj')
|
||||||
assert self.adjacency.dtype.kind in 'iu'
|
if self.adjacency.dtype.kind not in 'iu':
|
||||||
self.picture = None
|
raise Exception("adjacency array must have int or unsigned type.")
|
||||||
|
self._update()
|
||||||
if 'pos' in kwds:
|
if 'pos' in kwds:
|
||||||
self.pos = kwds['pos']
|
self.pos = kwds['pos']
|
||||||
self.picture = None
|
self._update()
|
||||||
if 'pen' in kwds:
|
if 'pen' in kwds:
|
||||||
self.setPen(kwds.pop('pen'))
|
self.setPen(kwds.pop('pen'))
|
||||||
self.picture = None
|
self._update()
|
||||||
|
|
||||||
if 'symbolPen' in kwds:
|
if 'symbolPen' in kwds:
|
||||||
kwds['pen'] = kwds.pop('symbolPen')
|
kwds['pen'] = kwds.pop('symbolPen')
|
||||||
|
if 'symbolBrush' in kwds:
|
||||||
|
kwds['brush'] = kwds.pop('symbolBrush')
|
||||||
self.scatter.setData(**kwds)
|
self.scatter.setData(**kwds)
|
||||||
self.informViewBoundsChanged()
|
self.informViewBoundsChanged()
|
||||||
|
|
||||||
def setPen(self, pen):
|
def _update(self):
|
||||||
self.pen = pen
|
|
||||||
self.picture = None
|
self.picture = None
|
||||||
|
self.prepareGeometryChange()
|
||||||
|
self.update()
|
||||||
|
|
||||||
|
def setPen(self, *args, **kwargs):
|
||||||
|
"""
|
||||||
|
Set the pen used to draw graph lines.
|
||||||
|
May be:
|
||||||
|
|
||||||
|
* None to disable line drawing
|
||||||
|
* Record array with fields (red, green, blue, alpha, width)
|
||||||
|
* Any set of arguments and keyword arguments accepted by
|
||||||
|
:func:`mkPen <pyqtgraph.mkPen>`.
|
||||||
|
* 'default' to use the default foreground color.
|
||||||
|
"""
|
||||||
|
if len(args) == 1 and len(kwargs) == 0:
|
||||||
|
self.pen = args[0]
|
||||||
|
else:
|
||||||
|
self.pen = fn.mkPen(*args, **kwargs)
|
||||||
|
self.picture = None
|
||||||
|
self.update()
|
||||||
|
|
||||||
def generatePicture(self):
|
def generatePicture(self):
|
||||||
self.picture = QtGui.QPicture()
|
self.picture = QtGui.QPicture()
|
||||||
|
@ -1,31 +1,11 @@
|
|||||||
from ..Qt import QtGui, QtCore
|
from ..Qt import QtGui, QtCore, isQObjectAlive
|
||||||
from ..GraphicsScene import GraphicsScene
|
from ..GraphicsScene import GraphicsScene
|
||||||
from ..Point import Point
|
from ..Point import Point
|
||||||
from .. import functions as fn
|
from .. import functions as fn
|
||||||
import weakref
|
import weakref
|
||||||
from ..pgcollections import OrderedDict
|
import operator
|
||||||
import operator, sys
|
from ..util.lru_cache import LRUCache
|
||||||
|
|
||||||
class FiniteCache(OrderedDict):
|
|
||||||
"""Caches a finite number of objects, removing
|
|
||||||
least-frequently used items."""
|
|
||||||
def __init__(self, length):
|
|
||||||
self._length = length
|
|
||||||
OrderedDict.__init__(self)
|
|
||||||
|
|
||||||
def __setitem__(self, item, val):
|
|
||||||
self.pop(item, None) # make sure item is added to end
|
|
||||||
OrderedDict.__setitem__(self, item, val)
|
|
||||||
while len(self) > self._length:
|
|
||||||
del self[list(self.keys())[0]]
|
|
||||||
|
|
||||||
def __getitem__(self, item):
|
|
||||||
val = OrderedDict.__getitem__(self, item)
|
|
||||||
del self[item]
|
|
||||||
self[item] = val ## promote this key
|
|
||||||
return val
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class GraphicsItem(object):
|
class GraphicsItem(object):
|
||||||
"""
|
"""
|
||||||
@ -38,7 +18,7 @@ class GraphicsItem(object):
|
|||||||
|
|
||||||
The GraphicsView system places a lot of emphasis on the notion that the graphics within the scene should be device independent--you should be able to take the same graphics and display them on screens of different resolutions, printers, export to SVG, etc. This is nice in principle, but causes me a lot of headache in practice. It means that I have to circumvent all the device-independent expectations any time I want to operate in pixel coordinates rather than arbitrary scene coordinates. A lot of the code in GraphicsItem is devoted to this task--keeping track of view widgets and device transforms, computing the size and shape of a pixel in local item coordinates, etc. Note that in item coordinates, a pixel does not have to be square or even rectangular, so just asking how to increase a bounding rect by 2px can be a rather complex task.
|
The GraphicsView system places a lot of emphasis on the notion that the graphics within the scene should be device independent--you should be able to take the same graphics and display them on screens of different resolutions, printers, export to SVG, etc. This is nice in principle, but causes me a lot of headache in practice. It means that I have to circumvent all the device-independent expectations any time I want to operate in pixel coordinates rather than arbitrary scene coordinates. A lot of the code in GraphicsItem is devoted to this task--keeping track of view widgets and device transforms, computing the size and shape of a pixel in local item coordinates, etc. Note that in item coordinates, a pixel does not have to be square or even rectangular, so just asking how to increase a bounding rect by 2px can be a rather complex task.
|
||||||
"""
|
"""
|
||||||
_pixelVectorGlobalCache = FiniteCache(100)
|
_pixelVectorGlobalCache = LRUCache(100, 70)
|
||||||
|
|
||||||
def __init__(self, register=True):
|
def __init__(self, register=True):
|
||||||
if not hasattr(self, '_qtBaseClass'):
|
if not hasattr(self, '_qtBaseClass'):
|
||||||
@ -62,8 +42,11 @@ class GraphicsItem(object):
|
|||||||
|
|
||||||
def getViewWidget(self):
|
def getViewWidget(self):
|
||||||
"""
|
"""
|
||||||
Return the view widget for this item. If the scene has multiple views, only the first view is returned.
|
Return the view widget for this item.
|
||||||
The return value is cached; clear the cached value with forgetViewWidget()
|
|
||||||
|
If the scene has multiple views, only the first view is returned.
|
||||||
|
The return value is cached; clear the cached value with forgetViewWidget().
|
||||||
|
If the view has been deleted by Qt, return None.
|
||||||
"""
|
"""
|
||||||
if self._viewWidget is None:
|
if self._viewWidget is None:
|
||||||
scene = self.scene()
|
scene = self.scene()
|
||||||
@ -73,7 +56,12 @@ class GraphicsItem(object):
|
|||||||
if len(views) < 1:
|
if len(views) < 1:
|
||||||
return None
|
return None
|
||||||
self._viewWidget = weakref.ref(self.scene().views()[0])
|
self._viewWidget = weakref.ref(self.scene().views()[0])
|
||||||
return self._viewWidget()
|
|
||||||
|
v = self._viewWidget()
|
||||||
|
if v is not None and not isQObjectAlive(v):
|
||||||
|
return None
|
||||||
|
|
||||||
|
return v
|
||||||
|
|
||||||
def forgetViewWidget(self):
|
def forgetViewWidget(self):
|
||||||
self._viewWidget = None
|
self._viewWidget = None
|
||||||
@ -479,24 +467,29 @@ class GraphicsItem(object):
|
|||||||
|
|
||||||
## disconnect from previous view
|
## disconnect from previous view
|
||||||
if oldView is not None:
|
if oldView is not None:
|
||||||
#print "disconnect:", self, oldView
|
for signal, slot in [('sigRangeChanged', self.viewRangeChanged),
|
||||||
try:
|
('sigDeviceRangeChanged', self.viewRangeChanged),
|
||||||
oldView.sigRangeChanged.disconnect(self.viewRangeChanged)
|
('sigTransformChanged', self.viewTransformChanged),
|
||||||
except TypeError:
|
('sigDeviceTransformChanged', self.viewTransformChanged)]:
|
||||||
pass
|
try:
|
||||||
|
getattr(oldView, signal).disconnect(slot)
|
||||||
try:
|
except (TypeError, AttributeError, RuntimeError):
|
||||||
oldView.sigTransformChanged.disconnect(self.viewTransformChanged)
|
# TypeError and RuntimeError are from pyqt and pyside, respectively
|
||||||
except TypeError:
|
pass
|
||||||
pass
|
|
||||||
|
|
||||||
self._connectedView = None
|
self._connectedView = None
|
||||||
|
|
||||||
## connect to new view
|
## connect to new view
|
||||||
if view is not None:
|
if view is not None:
|
||||||
#print "connect:", self, view
|
#print "connect:", self, view
|
||||||
view.sigRangeChanged.connect(self.viewRangeChanged)
|
if hasattr(view, 'sigDeviceRangeChanged'):
|
||||||
view.sigTransformChanged.connect(self.viewTransformChanged)
|
# connect signals from GraphicsView
|
||||||
|
view.sigDeviceRangeChanged.connect(self.viewRangeChanged)
|
||||||
|
view.sigDeviceTransformChanged.connect(self.viewTransformChanged)
|
||||||
|
else:
|
||||||
|
# connect signals from ViewBox
|
||||||
|
view.sigRangeChanged.connect(self.viewRangeChanged)
|
||||||
|
view.sigTransformChanged.connect(self.viewTransformChanged)
|
||||||
self._connectedView = weakref.ref(view)
|
self._connectedView = weakref.ref(view)
|
||||||
self.viewRangeChanged()
|
self.viewRangeChanged()
|
||||||
self.viewTransformChanged()
|
self.viewTransformChanged()
|
||||||
|
@ -31,6 +31,15 @@ class GraphicsLayout(GraphicsWidget):
|
|||||||
#ret = GraphicsWidget.resizeEvent(self, ev)
|
#ret = GraphicsWidget.resizeEvent(self, ev)
|
||||||
#print self.pos(), self.mapToDevice(self.rect().topLeft())
|
#print self.pos(), self.mapToDevice(self.rect().topLeft())
|
||||||
#return ret
|
#return ret
|
||||||
|
|
||||||
|
def setBorder(self, *args, **kwds):
|
||||||
|
"""
|
||||||
|
Set the pen used to draw border between cells.
|
||||||
|
|
||||||
|
See :func:`mkPen <pyqtgraph.mkPen>` for arguments.
|
||||||
|
"""
|
||||||
|
self.border = fn.mkPen(*args, **kwds)
|
||||||
|
self.update()
|
||||||
|
|
||||||
def nextRow(self):
|
def nextRow(self):
|
||||||
"""Advance to next row for automatic item placement"""
|
"""Advance to next row for automatic item placement"""
|
||||||
|
@ -21,8 +21,15 @@ class GraphicsObject(GraphicsItem, QtGui.QGraphicsObject):
|
|||||||
ret = QtGui.QGraphicsObject.itemChange(self, change, value)
|
ret = QtGui.QGraphicsObject.itemChange(self, change, value)
|
||||||
if change in [self.ItemParentHasChanged, self.ItemSceneHasChanged]:
|
if change in [self.ItemParentHasChanged, self.ItemSceneHasChanged]:
|
||||||
self.parentChanged()
|
self.parentChanged()
|
||||||
if self.__inform_view_on_changes and change in [self.ItemPositionHasChanged, self.ItemTransformHasChanged]:
|
try:
|
||||||
self.informViewBoundsChanged()
|
inform_view_on_change = self.__inform_view_on_changes
|
||||||
|
except AttributeError:
|
||||||
|
# It's possible that the attribute was already collected when the itemChange happened
|
||||||
|
# (if it was triggered during the gc of the object).
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
if inform_view_on_change and change in [self.ItemPositionHasChanged, self.ItemTransformHasChanged]:
|
||||||
|
self.informViewBoundsChanged()
|
||||||
|
|
||||||
## workaround for pyqt bug:
|
## workaround for pyqt bug:
|
||||||
## http://www.riverbankcomputing.com/pipermail/pyqt/2012-August/031818.html
|
## http://www.riverbankcomputing.com/pipermail/pyqt/2012-August/031818.html
|
||||||
|
@ -58,7 +58,7 @@ class HistogramLUTItem(GraphicsWidget):
|
|||||||
self.region = LinearRegionItem([0, 1], LinearRegionItem.Horizontal)
|
self.region = LinearRegionItem([0, 1], LinearRegionItem.Horizontal)
|
||||||
self.region.setZValue(1000)
|
self.region.setZValue(1000)
|
||||||
self.vb.addItem(self.region)
|
self.vb.addItem(self.region)
|
||||||
self.axis = AxisItem('left', linkView=self.vb, maxTickLength=-10, showValues=False)
|
self.axis = AxisItem('left', linkView=self.vb, maxTickLength=-10)
|
||||||
self.layout.addItem(self.axis, 0, 0)
|
self.layout.addItem(self.axis, 0, 0)
|
||||||
self.layout.addItem(self.vb, 0, 1)
|
self.layout.addItem(self.vb, 0, 1)
|
||||||
self.layout.addItem(self.gradient, 0, 2)
|
self.layout.addItem(self.gradient, 0, 2)
|
||||||
|
@ -6,6 +6,7 @@ import collections
|
|||||||
from .. import functions as fn
|
from .. import functions as fn
|
||||||
from .. import debug as debug
|
from .. import debug as debug
|
||||||
from .GraphicsObject import GraphicsObject
|
from .GraphicsObject import GraphicsObject
|
||||||
|
from ..Point import Point
|
||||||
|
|
||||||
__all__ = ['ImageItem']
|
__all__ = ['ImageItem']
|
||||||
class ImageItem(GraphicsObject):
|
class ImageItem(GraphicsObject):
|
||||||
@ -34,20 +35,16 @@ class ImageItem(GraphicsObject):
|
|||||||
See :func:`setImage <pyqtgraph.ImageItem.setImage>` for all allowed initialization arguments.
|
See :func:`setImage <pyqtgraph.ImageItem.setImage>` for all allowed initialization arguments.
|
||||||
"""
|
"""
|
||||||
GraphicsObject.__init__(self)
|
GraphicsObject.__init__(self)
|
||||||
#self.pixmapItem = QtGui.QGraphicsPixmapItem(self)
|
|
||||||
#self.qimage = QtGui.QImage()
|
|
||||||
#self._pixmap = None
|
|
||||||
self.menu = None
|
self.menu = None
|
||||||
self.image = None ## original image data
|
self.image = None ## original image data
|
||||||
self.qimage = None ## rendered image for display
|
self.qimage = None ## rendered image for display
|
||||||
#self.clipMask = None
|
|
||||||
|
|
||||||
self.paintMode = None
|
self.paintMode = None
|
||||||
|
|
||||||
self.levels = None ## [min, max] or [[redMin, redMax], ...]
|
self.levels = None ## [min, max] or [[redMin, redMax], ...]
|
||||||
self.lut = None
|
self.lut = None
|
||||||
|
self.autoDownsample = False
|
||||||
|
|
||||||
#self.clipLevel = None
|
|
||||||
self.drawKernel = None
|
self.drawKernel = None
|
||||||
self.border = None
|
self.border = None
|
||||||
self.removable = False
|
self.removable = False
|
||||||
@ -142,7 +139,18 @@ class ImageItem(GraphicsObject):
|
|||||||
if update:
|
if update:
|
||||||
self.updateImage()
|
self.updateImage()
|
||||||
|
|
||||||
|
def setAutoDownsample(self, ads):
|
||||||
|
"""
|
||||||
|
Set the automatic downsampling mode for this ImageItem.
|
||||||
|
|
||||||
|
Added in version 0.9.9
|
||||||
|
"""
|
||||||
|
self.autoDownsample = ads
|
||||||
|
self.qimage = None
|
||||||
|
self.update()
|
||||||
|
|
||||||
def setOpts(self, update=True, **kargs):
|
def setOpts(self, update=True, **kargs):
|
||||||
|
|
||||||
if 'lut' in kargs:
|
if 'lut' in kargs:
|
||||||
self.setLookupTable(kargs['lut'], update=update)
|
self.setLookupTable(kargs['lut'], update=update)
|
||||||
if 'levels' in kargs:
|
if 'levels' in kargs:
|
||||||
@ -158,6 +166,10 @@ class ImageItem(GraphicsObject):
|
|||||||
if 'removable' in kargs:
|
if 'removable' in kargs:
|
||||||
self.removable = kargs['removable']
|
self.removable = kargs['removable']
|
||||||
self.menu = None
|
self.menu = None
|
||||||
|
if 'autoDownsample' in kargs:
|
||||||
|
self.setAutoDownsample(kargs['autoDownsample'])
|
||||||
|
if update:
|
||||||
|
self.update()
|
||||||
|
|
||||||
def setRect(self, rect):
|
def setRect(self, rect):
|
||||||
"""Scale and translate the image to fit within rect (must be a QRect or QRectF)."""
|
"""Scale and translate the image to fit within rect (must be a QRect or QRectF)."""
|
||||||
@ -188,6 +200,9 @@ class ImageItem(GraphicsObject):
|
|||||||
opacity (float 0.0-1.0)
|
opacity (float 0.0-1.0)
|
||||||
compositionMode see :func:`setCompositionMode <pyqtgraph.ImageItem.setCompositionMode>`
|
compositionMode see :func:`setCompositionMode <pyqtgraph.ImageItem.setCompositionMode>`
|
||||||
border Sets the pen used when drawing the image border. Default is None.
|
border Sets the pen used when drawing the image border. Default is None.
|
||||||
|
autoDownsample (bool) If True, the image is automatically downsampled to match the
|
||||||
|
screen resolution. This improves performance for large images and
|
||||||
|
reduces aliasing.
|
||||||
================= =========================================================================
|
================= =========================================================================
|
||||||
"""
|
"""
|
||||||
profile = debug.Profiler()
|
profile = debug.Profiler()
|
||||||
@ -200,6 +215,9 @@ class ImageItem(GraphicsObject):
|
|||||||
gotNewData = True
|
gotNewData = True
|
||||||
shapeChanged = (self.image is None or image.shape != self.image.shape)
|
shapeChanged = (self.image is None or image.shape != self.image.shape)
|
||||||
self.image = image.view(np.ndarray)
|
self.image = image.view(np.ndarray)
|
||||||
|
if self.image.shape[0] > 2**15-1 or self.image.shape[1] > 2**15-1:
|
||||||
|
if 'autoDownsample' not in kargs:
|
||||||
|
kargs['autoDownsample'] = True
|
||||||
if shapeChanged:
|
if shapeChanged:
|
||||||
self.prepareGeometryChange()
|
self.prepareGeometryChange()
|
||||||
self.informViewBoundsChanged()
|
self.informViewBoundsChanged()
|
||||||
@ -246,11 +264,10 @@ class ImageItem(GraphicsObject):
|
|||||||
}
|
}
|
||||||
defaults.update(kargs)
|
defaults.update(kargs)
|
||||||
return self.setImage(*args, **defaults)
|
return self.setImage(*args, **defaults)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def render(self):
|
def render(self):
|
||||||
|
# Convert data to QImage for display.
|
||||||
|
|
||||||
profile = debug.Profiler()
|
profile = debug.Profiler()
|
||||||
if self.image is None or self.image.size == 0:
|
if self.image is None or self.image.size == 0:
|
||||||
return
|
return
|
||||||
@ -258,10 +275,22 @@ class ImageItem(GraphicsObject):
|
|||||||
lut = self.lut(self.image)
|
lut = self.lut(self.image)
|
||||||
else:
|
else:
|
||||||
lut = self.lut
|
lut = self.lut
|
||||||
#print lut.shape
|
|
||||||
#print self.lut
|
if self.autoDownsample:
|
||||||
|
# reduce dimensions of image based on screen resolution
|
||||||
argb, alpha = fn.makeARGB(self.image.transpose((1, 0, 2)[:self.image.ndim]), lut=lut, levels=self.levels)
|
o = self.mapToDevice(QtCore.QPointF(0,0))
|
||||||
|
x = self.mapToDevice(QtCore.QPointF(1,0))
|
||||||
|
y = self.mapToDevice(QtCore.QPointF(0,1))
|
||||||
|
w = Point(x-o).length()
|
||||||
|
h = Point(y-o).length()
|
||||||
|
xds = max(1, int(1/w))
|
||||||
|
yds = max(1, int(1/h))
|
||||||
|
image = fn.downsample(self.image, xds, axis=0)
|
||||||
|
image = fn.downsample(image, yds, axis=1)
|
||||||
|
else:
|
||||||
|
image = self.image
|
||||||
|
|
||||||
|
argb, alpha = fn.makeARGB(image.transpose((1, 0, 2)[:image.ndim]), lut=lut, levels=self.levels)
|
||||||
self.qimage = fn.makeQImage(argb, alpha, transpose=False)
|
self.qimage = fn.makeQImage(argb, alpha, transpose=False)
|
||||||
|
|
||||||
def paint(self, p, *args):
|
def paint(self, p, *args):
|
||||||
@ -277,7 +306,7 @@ class ImageItem(GraphicsObject):
|
|||||||
p.setCompositionMode(self.paintMode)
|
p.setCompositionMode(self.paintMode)
|
||||||
profile('set comp mode')
|
profile('set comp mode')
|
||||||
|
|
||||||
p.drawImage(QtCore.QPointF(0,0), self.qimage)
|
p.drawImage(QtCore.QRectF(0,0,self.image.shape[0],self.image.shape[1]), self.qimage)
|
||||||
profile('p.drawImage')
|
profile('p.drawImage')
|
||||||
if self.border is not None:
|
if self.border is not None:
|
||||||
p.setPen(self.border)
|
p.setPen(self.border)
|
||||||
@ -322,6 +351,8 @@ class ImageItem(GraphicsObject):
|
|||||||
mx = stepData.max()
|
mx = stepData.max()
|
||||||
step = np.ceil((mx-mn) / 500.)
|
step = np.ceil((mx-mn) / 500.)
|
||||||
bins = np.arange(mn, mx+1.01*step, step, dtype=np.int)
|
bins = np.arange(mn, mx+1.01*step, step, dtype=np.int)
|
||||||
|
if len(bins) == 0:
|
||||||
|
bins = [mn, mx]
|
||||||
else:
|
else:
|
||||||
bins = 500
|
bins = 500
|
||||||
|
|
||||||
@ -355,6 +386,11 @@ class ImageItem(GraphicsObject):
|
|||||||
if self.image is None:
|
if self.image is None:
|
||||||
return 1,1
|
return 1,1
|
||||||
return br.width()/self.width(), br.height()/self.height()
|
return br.width()/self.width(), br.height()/self.height()
|
||||||
|
|
||||||
|
def viewTransformChanged(self):
|
||||||
|
if self.autoDownsample:
|
||||||
|
self.qimage = None
|
||||||
|
self.update()
|
||||||
|
|
||||||
#def mousePressEvent(self, ev):
|
#def mousePressEvent(self, ev):
|
||||||
#if self.drawKernel is not None and ev.button() == QtCore.Qt.LeftButton:
|
#if self.drawKernel is not None and ev.button() == QtCore.Qt.LeftButton:
|
||||||
|
@ -15,7 +15,7 @@ class InfiniteLine(GraphicsObject):
|
|||||||
This line may be dragged to indicate a position in data coordinates.
|
This line may be dragged to indicate a position in data coordinates.
|
||||||
|
|
||||||
=============================== ===================================================
|
=============================== ===================================================
|
||||||
**Signals**
|
**Signals:**
|
||||||
sigDragged(self)
|
sigDragged(self)
|
||||||
sigPositionChangeFinished(self)
|
sigPositionChangeFinished(self)
|
||||||
sigPositionChanged(self)
|
sigPositionChanged(self)
|
||||||
@ -28,18 +28,18 @@ class InfiniteLine(GraphicsObject):
|
|||||||
|
|
||||||
def __init__(self, pos=None, angle=90, pen=None, movable=False, bounds=None):
|
def __init__(self, pos=None, angle=90, pen=None, movable=False, bounds=None):
|
||||||
"""
|
"""
|
||||||
============= ==================================================================
|
=============== ==================================================================
|
||||||
**Arguments**
|
**Arguments:**
|
||||||
pos Position of the line. This can be a QPointF or a single value for
|
pos Position of the line. This can be a QPointF or a single value for
|
||||||
vertical/horizontal lines.
|
vertical/horizontal lines.
|
||||||
angle Angle of line in degrees. 0 is horizontal, 90 is vertical.
|
angle Angle of line in degrees. 0 is horizontal, 90 is vertical.
|
||||||
pen Pen to use when drawing line. Can be any arguments that are valid
|
pen Pen to use when drawing line. Can be any arguments that are valid
|
||||||
for :func:`mkPen <pyqtgraph.mkPen>`. Default pen is transparent
|
for :func:`mkPen <pyqtgraph.mkPen>`. Default pen is transparent
|
||||||
yellow.
|
yellow.
|
||||||
movable If True, the line can be dragged to a new position by the user.
|
movable If True, the line can be dragged to a new position by the user.
|
||||||
bounds Optional [min, max] bounding values. Bounds are only valid if the
|
bounds Optional [min, max] bounding values. Bounds are only valid if the
|
||||||
line is vertical or horizontal.
|
line is vertical or horizontal.
|
||||||
============= ==================================================================
|
=============== ==================================================================
|
||||||
"""
|
"""
|
||||||
|
|
||||||
GraphicsObject.__init__(self)
|
GraphicsObject.__init__(self)
|
||||||
@ -73,10 +73,10 @@ class InfiniteLine(GraphicsObject):
|
|||||||
self.maxRange = bounds
|
self.maxRange = bounds
|
||||||
self.setValue(self.value())
|
self.setValue(self.value())
|
||||||
|
|
||||||
def setPen(self, pen):
|
def setPen(self, *args, **kwargs):
|
||||||
"""Set the pen for drawing the line. Allowable arguments are any that are valid
|
"""Set the pen for drawing the line. Allowable arguments are any that are valid
|
||||||
for :func:`mkPen <pyqtgraph.mkPen>`."""
|
for :func:`mkPen <pyqtgraph.mkPen>`."""
|
||||||
self.pen = fn.mkPen(pen)
|
self.pen = fn.mkPen(*args, **kwargs)
|
||||||
self.currentPen = self.pen
|
self.currentPen = self.pen
|
||||||
self.update()
|
self.update()
|
||||||
|
|
||||||
|
@ -18,14 +18,14 @@ class IsocurveItem(GraphicsObject):
|
|||||||
"""
|
"""
|
||||||
Create a new isocurve item.
|
Create a new isocurve item.
|
||||||
|
|
||||||
============= ===============================================================
|
============== ===============================================================
|
||||||
**Arguments**
|
**Arguments:**
|
||||||
data A 2-dimensional ndarray. Can be initialized as None, and set
|
data A 2-dimensional ndarray. Can be initialized as None, and set
|
||||||
later using :func:`setData <pyqtgraph.IsocurveItem.setData>`
|
later using :func:`setData <pyqtgraph.IsocurveItem.setData>`
|
||||||
level The cutoff value at which to draw the isocurve.
|
level The cutoff value at which to draw the isocurve.
|
||||||
pen The color of the curve item. Can be anything valid for
|
pen The color of the curve item. Can be anything valid for
|
||||||
:func:`mkPen <pyqtgraph.mkPen>`
|
:func:`mkPen <pyqtgraph.mkPen>`
|
||||||
============= ===============================================================
|
============== ===============================================================
|
||||||
"""
|
"""
|
||||||
GraphicsObject.__init__(self)
|
GraphicsObject.__init__(self)
|
||||||
|
|
||||||
@ -45,12 +45,12 @@ class IsocurveItem(GraphicsObject):
|
|||||||
"""
|
"""
|
||||||
Set the data/image to draw isocurves for.
|
Set the data/image to draw isocurves for.
|
||||||
|
|
||||||
============= ========================================================================
|
============== ========================================================================
|
||||||
**Arguments**
|
**Arguments:**
|
||||||
data A 2-dimensional ndarray.
|
data A 2-dimensional ndarray.
|
||||||
level The cutoff value at which to draw the curve. If level is not specified,
|
level The cutoff value at which to draw the curve. If level is not specified,
|
||||||
the previously set level is used.
|
the previously set level is used.
|
||||||
============= ========================================================================
|
============== ========================================================================
|
||||||
"""
|
"""
|
||||||
if level is None:
|
if level is None:
|
||||||
level = self.level
|
level = self.level
|
||||||
|
@ -21,17 +21,17 @@ class LegendItem(GraphicsWidget, GraphicsWidgetAnchor):
|
|||||||
"""
|
"""
|
||||||
def __init__(self, size=None, offset=None):
|
def __init__(self, size=None, offset=None):
|
||||||
"""
|
"""
|
||||||
========== ===============================================================
|
============== ===============================================================
|
||||||
Arguments
|
**Arguments:**
|
||||||
size Specifies the fixed size (width, height) of the legend. If
|
size Specifies the fixed size (width, height) of the legend. If
|
||||||
this argument is omitted, the legend will autimatically resize
|
this argument is omitted, the legend will autimatically resize
|
||||||
to fit its contents.
|
to fit its contents.
|
||||||
offset Specifies the offset position relative to the legend's parent.
|
offset Specifies the offset position relative to the legend's parent.
|
||||||
Positive values offset from the left or top; negative values
|
Positive values offset from the left or top; negative values
|
||||||
offset from the right or bottom. If offset is None, the
|
offset from the right or bottom. If offset is None, the
|
||||||
legend must be anchored manually by calling anchor() or
|
legend must be anchored manually by calling anchor() or
|
||||||
positioned by calling setPos().
|
positioned by calling setPos().
|
||||||
========== ===============================================================
|
============== ===============================================================
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@ -61,14 +61,14 @@ class LegendItem(GraphicsWidget, GraphicsWidgetAnchor):
|
|||||||
"""
|
"""
|
||||||
Add a new entry to the legend.
|
Add a new entry to the legend.
|
||||||
|
|
||||||
=========== ========================================================
|
============== ========================================================
|
||||||
Arguments
|
**Arguments:**
|
||||||
item A PlotDataItem from which the line and point style
|
item A PlotDataItem from which the line and point style
|
||||||
of the item will be determined or an instance of
|
of the item will be determined or an instance of
|
||||||
ItemSample (or a subclass), allowing the item display
|
ItemSample (or a subclass), allowing the item display
|
||||||
to be customized.
|
to be customized.
|
||||||
title The title to display for this item. Simple HTML allowed.
|
title The title to display for this item. Simple HTML allowed.
|
||||||
=========== ========================================================
|
============== ========================================================
|
||||||
"""
|
"""
|
||||||
label = LabelItem(name)
|
label = LabelItem(name)
|
||||||
if isinstance(item, ItemSample):
|
if isinstance(item, ItemSample):
|
||||||
@ -85,10 +85,10 @@ class LegendItem(GraphicsWidget, GraphicsWidgetAnchor):
|
|||||||
"""
|
"""
|
||||||
Removes one item from the legend.
|
Removes one item from the legend.
|
||||||
|
|
||||||
=========== ========================================================
|
============== ========================================================
|
||||||
Arguments
|
**Arguments:**
|
||||||
title The title displayed for this item.
|
title The title displayed for this item.
|
||||||
=========== ========================================================
|
============== ========================================================
|
||||||
"""
|
"""
|
||||||
# Thanks, Ulrich!
|
# Thanks, Ulrich!
|
||||||
# cycle for a match
|
# cycle for a match
|
||||||
|
@ -30,19 +30,19 @@ class LinearRegionItem(UIGraphicsItem):
|
|||||||
def __init__(self, values=[0,1], orientation=None, brush=None, movable=True, bounds=None):
|
def __init__(self, values=[0,1], orientation=None, brush=None, movable=True, bounds=None):
|
||||||
"""Create a new LinearRegionItem.
|
"""Create a new LinearRegionItem.
|
||||||
|
|
||||||
============= =====================================================================
|
============== =====================================================================
|
||||||
**Arguments**
|
**Arguments:**
|
||||||
values A list of the positions of the lines in the region. These are not
|
values A list of the positions of the lines in the region. These are not
|
||||||
limits; limits can be set by specifying bounds.
|
limits; limits can be set by specifying bounds.
|
||||||
orientation Options are LinearRegionItem.Vertical or LinearRegionItem.Horizontal.
|
orientation Options are LinearRegionItem.Vertical or LinearRegionItem.Horizontal.
|
||||||
If not specified it will be vertical.
|
If not specified it will be vertical.
|
||||||
brush Defines the brush that fills the region. Can be any arguments that
|
brush Defines the brush that fills the region. Can be any arguments that
|
||||||
are valid for :func:`mkBrush <pyqtgraph.mkBrush>`. Default is
|
are valid for :func:`mkBrush <pyqtgraph.mkBrush>`. Default is
|
||||||
transparent blue.
|
transparent blue.
|
||||||
movable If True, the region and individual lines are movable by the user; if
|
movable If True, the region and individual lines are movable by the user; if
|
||||||
False, they are static.
|
False, they are static.
|
||||||
bounds Optional [min, max] bounding values for the region
|
bounds Optional [min, max] bounding values for the region
|
||||||
============= =====================================================================
|
============== =====================================================================
|
||||||
"""
|
"""
|
||||||
|
|
||||||
UIGraphicsItem.__init__(self)
|
UIGraphicsItem.__init__(self)
|
||||||
@ -89,10 +89,10 @@ class LinearRegionItem(UIGraphicsItem):
|
|||||||
def setRegion(self, rgn):
|
def setRegion(self, rgn):
|
||||||
"""Set the values for the edges of the region.
|
"""Set the values for the edges of the region.
|
||||||
|
|
||||||
============= ==============================================
|
============== ==============================================
|
||||||
**Arguments**
|
**Arguments:**
|
||||||
rgn A list or tuple of the lower and upper values.
|
rgn A list or tuple of the lower and upper values.
|
||||||
============= ==============================================
|
============== ==============================================
|
||||||
"""
|
"""
|
||||||
if self.lines[0].value() == rgn[0] and self.lines[1].value() == rgn[1]:
|
if self.lines[0].value() == rgn[0] and self.lines[1].value() == rgn[1]:
|
||||||
return
|
return
|
||||||
|
@ -7,26 +7,23 @@ Distributed under MIT/X11 license. See license.txt for more infomation.
|
|||||||
|
|
||||||
from numpy import ndarray
|
from numpy import ndarray
|
||||||
from . import GraphicsLayout
|
from . import GraphicsLayout
|
||||||
|
from ..metaarray import *
|
||||||
|
|
||||||
try:
|
|
||||||
from metaarray import *
|
|
||||||
HAVE_METAARRAY = True
|
|
||||||
except:
|
|
||||||
#raise
|
|
||||||
HAVE_METAARRAY = False
|
|
||||||
|
|
||||||
|
|
||||||
__all__ = ['MultiPlotItem']
|
__all__ = ['MultiPlotItem']
|
||||||
class MultiPlotItem(GraphicsLayout.GraphicsLayout):
|
class MultiPlotItem(GraphicsLayout.GraphicsLayout):
|
||||||
"""
|
"""
|
||||||
Automaticaly generates a grid of plots from a multi-dimensional array
|
Automatically generates a grid of plots from a multi-dimensional array
|
||||||
"""
|
"""
|
||||||
|
def __init__(self, *args, **kwds):
|
||||||
|
GraphicsLayout.GraphicsLayout.__init__(self, *args, **kwds)
|
||||||
|
self.plots = []
|
||||||
|
|
||||||
|
|
||||||
def plot(self, data):
|
def plot(self, data):
|
||||||
#self.layout.clear()
|
#self.layout.clear()
|
||||||
self.plots = []
|
|
||||||
|
if hasattr(data, 'implements') and data.implements('MetaArray'):
|
||||||
if HAVE_METAARRAY and (hasattr(data, 'implements') and data.implements('MetaArray')):
|
|
||||||
if data.ndim != 2:
|
if data.ndim != 2:
|
||||||
raise Exception("MultiPlot currently only accepts 2D MetaArray.")
|
raise Exception("MultiPlot currently only accepts 2D MetaArray.")
|
||||||
ic = data.infoCopy()
|
ic = data.infoCopy()
|
||||||
@ -44,21 +41,17 @@ class MultiPlotItem(GraphicsLayout.GraphicsLayout):
|
|||||||
pi.plot(data[tuple(sl)])
|
pi.plot(data[tuple(sl)])
|
||||||
#self.layout.addItem(pi, i, 0)
|
#self.layout.addItem(pi, i, 0)
|
||||||
self.plots.append((pi, i, 0))
|
self.plots.append((pi, i, 0))
|
||||||
title = None
|
|
||||||
units = None
|
|
||||||
info = ic[ax]['cols'][i]
|
info = ic[ax]['cols'][i]
|
||||||
if 'title' in info:
|
title = info.get('title', info.get('name', None))
|
||||||
title = info['title']
|
units = info.get('units', None)
|
||||||
elif 'name' in info:
|
|
||||||
title = info['name']
|
|
||||||
if 'units' in info:
|
|
||||||
units = info['units']
|
|
||||||
|
|
||||||
pi.setLabel('left', text=title, units=units)
|
pi.setLabel('left', text=title, units=units)
|
||||||
|
info = ic[1-ax]
|
||||||
|
title = info.get('title', info.get('name', None))
|
||||||
|
units = info.get('units', None)
|
||||||
|
pi.setLabel('bottom', text=title, units=units)
|
||||||
else:
|
else:
|
||||||
raise Exception("Data type %s not (yet?) supported for MultiPlot." % type(data))
|
raise Exception("Data type %s not (yet?) supported for MultiPlot." % type(data))
|
||||||
|
|
||||||
def close(self):
|
def close(self):
|
||||||
for p in self.plots:
|
for p in self.plots:
|
||||||
p[0].close()
|
p[0].close()
|
||||||
|
@ -173,8 +173,14 @@ class PlotCurveItem(GraphicsObject):
|
|||||||
if pxPad > 0:
|
if pxPad > 0:
|
||||||
# determine length of pixel in local x, y directions
|
# determine length of pixel in local x, y directions
|
||||||
px, py = self.pixelVectors()
|
px, py = self.pixelVectors()
|
||||||
px = 0 if px is None else px.length()
|
try:
|
||||||
py = 0 if py is None else py.length()
|
px = 0 if px is None else px.length()
|
||||||
|
except OverflowError:
|
||||||
|
px = 0
|
||||||
|
try:
|
||||||
|
py = 0 if py is None else py.length()
|
||||||
|
except OverflowError:
|
||||||
|
py = 0
|
||||||
|
|
||||||
# return bounds expanded by pixel size
|
# return bounds expanded by pixel size
|
||||||
px *= pxPad
|
px *= pxPad
|
||||||
@ -486,7 +492,7 @@ class PlotCurveItem(GraphicsObject):
|
|||||||
gl.glStencilOp(gl.GL_REPLACE, gl.GL_KEEP, gl.GL_KEEP)
|
gl.glStencilOp(gl.GL_REPLACE, gl.GL_KEEP, gl.GL_KEEP)
|
||||||
|
|
||||||
## draw stencil pattern
|
## draw stencil pattern
|
||||||
gl.glStencilMask(0xFF);
|
gl.glStencilMask(0xFF)
|
||||||
gl.glClear(gl.GL_STENCIL_BUFFER_BIT)
|
gl.glClear(gl.GL_STENCIL_BUFFER_BIT)
|
||||||
gl.glBegin(gl.GL_TRIANGLES)
|
gl.glBegin(gl.GL_TRIANGLES)
|
||||||
gl.glVertex2f(rect.x(), rect.y())
|
gl.glVertex2f(rect.x(), rect.y())
|
||||||
@ -520,7 +526,7 @@ class PlotCurveItem(GraphicsObject):
|
|||||||
gl.glEnable(gl.GL_LINE_SMOOTH)
|
gl.glEnable(gl.GL_LINE_SMOOTH)
|
||||||
gl.glEnable(gl.GL_BLEND)
|
gl.glEnable(gl.GL_BLEND)
|
||||||
gl.glBlendFunc(gl.GL_SRC_ALPHA, gl.GL_ONE_MINUS_SRC_ALPHA)
|
gl.glBlendFunc(gl.GL_SRC_ALPHA, gl.GL_ONE_MINUS_SRC_ALPHA)
|
||||||
gl.glHint(gl.GL_LINE_SMOOTH_HINT, gl.GL_NICEST);
|
gl.glHint(gl.GL_LINE_SMOOTH_HINT, gl.GL_NICEST)
|
||||||
gl.glDrawArrays(gl.GL_LINE_STRIP, 0, pos.size / pos.shape[-1])
|
gl.glDrawArrays(gl.GL_LINE_STRIP, 0, pos.size / pos.shape[-1])
|
||||||
finally:
|
finally:
|
||||||
gl.glDisableClientState(gl.GL_VERTEX_ARRAY)
|
gl.glDisableClientState(gl.GL_VERTEX_ARRAY)
|
||||||
|
@ -56,10 +56,11 @@ class PlotDataItem(GraphicsObject):
|
|||||||
=========================== =========================================
|
=========================== =========================================
|
||||||
|
|
||||||
**Line style keyword arguments:**
|
**Line style keyword arguments:**
|
||||||
========== ================================================
|
|
||||||
connect Specifies how / whether vertexes should be connected.
|
========== ==============================================================================
|
||||||
See :func:`arrayToQPath() <pyqtgraph.arrayToQPath>`
|
connect Specifies how / whether vertexes should be connected. See
|
||||||
pen Pen to use for drawing line between points.
|
:func:`arrayToQPath() <pyqtgraph.arrayToQPath>`
|
||||||
|
pen Pen to use for drawing line between points.
|
||||||
Default is solid grey, 1px width. Use None to disable line drawing.
|
Default is solid grey, 1px width. Use None to disable line drawing.
|
||||||
May be any single argument accepted by :func:`mkPen() <pyqtgraph.mkPen>`
|
May be any single argument accepted by :func:`mkPen() <pyqtgraph.mkPen>`
|
||||||
shadowPen Pen for secondary line to draw behind the primary line. disabled by default.
|
shadowPen Pen for secondary line to draw behind the primary line. disabled by default.
|
||||||
@ -67,21 +68,29 @@ class PlotDataItem(GraphicsObject):
|
|||||||
fillLevel Fill the area between the curve and fillLevel
|
fillLevel Fill the area between the curve and fillLevel
|
||||||
fillBrush Fill to use when fillLevel is specified.
|
fillBrush Fill to use when fillLevel is specified.
|
||||||
May be any single argument accepted by :func:`mkBrush() <pyqtgraph.mkBrush>`
|
May be any single argument accepted by :func:`mkBrush() <pyqtgraph.mkBrush>`
|
||||||
========== ================================================
|
stepMode If True, two orthogonal lines are drawn for each sample
|
||||||
|
as steps. This is commonly used when drawing histograms.
|
||||||
|
Note that in this case, `len(x) == len(y) + 1`
|
||||||
|
(added in version 0.9.9)
|
||||||
|
========== ==============================================================================
|
||||||
|
|
||||||
**Point style keyword arguments:** (see :func:`ScatterPlotItem.setData() <pyqtgraph.ScatterPlotItem.setData>` for more information)
|
**Point style keyword arguments:** (see :func:`ScatterPlotItem.setData() <pyqtgraph.ScatterPlotItem.setData>` for more information)
|
||||||
|
|
||||||
============ ================================================
|
============ =====================================================
|
||||||
symbol Symbol to use for drawing points OR list of symbols, one per point. Default is no symbol.
|
symbol Symbol to use for drawing points OR list of symbols,
|
||||||
|
one per point. Default is no symbol.
|
||||||
Options are o, s, t, d, +, or any QPainterPath
|
Options are o, s, t, d, +, or any QPainterPath
|
||||||
symbolPen Outline pen for drawing points OR list of pens, one per point.
|
symbolPen Outline pen for drawing points OR list of pens, one
|
||||||
May be any single argument accepted by :func:`mkPen() <pyqtgraph.mkPen>`
|
per point. May be any single argument accepted by
|
||||||
symbolBrush Brush for filling points OR list of brushes, one per point.
|
:func:`mkPen() <pyqtgraph.mkPen>`
|
||||||
May be any single argument accepted by :func:`mkBrush() <pyqtgraph.mkBrush>`
|
symbolBrush Brush for filling points OR list of brushes, one per
|
||||||
|
point. May be any single argument accepted by
|
||||||
|
:func:`mkBrush() <pyqtgraph.mkBrush>`
|
||||||
symbolSize Diameter of symbols OR list of diameters.
|
symbolSize Diameter of symbols OR list of diameters.
|
||||||
pxMode (bool) If True, then symbolSize is specified in pixels. If False, then symbolSize is
|
pxMode (bool) If True, then symbolSize is specified in
|
||||||
|
pixels. If False, then symbolSize is
|
||||||
specified in data coordinates.
|
specified in data coordinates.
|
||||||
============ ================================================
|
============ =====================================================
|
||||||
|
|
||||||
**Optimization keyword arguments:**
|
**Optimization keyword arguments:**
|
||||||
|
|
||||||
@ -92,11 +101,11 @@ class PlotDataItem(GraphicsObject):
|
|||||||
decimate deprecated.
|
decimate deprecated.
|
||||||
downsample (int) Reduce the number of samples displayed by this value
|
downsample (int) Reduce the number of samples displayed by this value
|
||||||
downsampleMethod 'subsample': Downsample by taking the first of N samples.
|
downsampleMethod 'subsample': Downsample by taking the first of N samples.
|
||||||
This method is fastest and least accurate.
|
This method is fastest and least accurate.
|
||||||
'mean': Downsample by taking the mean of N samples.
|
'mean': Downsample by taking the mean of N samples.
|
||||||
'peak': Downsample by drawing a saw wave that follows the min
|
'peak': Downsample by drawing a saw wave that follows the min
|
||||||
and max of the original data. This method produces the best
|
and max of the original data. This method produces the best
|
||||||
visual representation of the data but is slower.
|
visual representation of the data but is slower.
|
||||||
autoDownsample (bool) If True, resample the data before plotting to avoid plotting
|
autoDownsample (bool) If True, resample the data before plotting to avoid plotting
|
||||||
multiple line segments per pixel. This can improve performance when
|
multiple line segments per pixel. This can improve performance when
|
||||||
viewing very high-density data, but increases the initial overhead
|
viewing very high-density data, but increases the initial overhead
|
||||||
@ -145,6 +154,7 @@ class PlotDataItem(GraphicsObject):
|
|||||||
'shadowPen': None,
|
'shadowPen': None,
|
||||||
'fillLevel': None,
|
'fillLevel': None,
|
||||||
'fillBrush': None,
|
'fillBrush': None,
|
||||||
|
'stepMode': None,
|
||||||
|
|
||||||
'symbol': None,
|
'symbol': None,
|
||||||
'symbolSize': 10,
|
'symbolSize': 10,
|
||||||
@ -290,18 +300,18 @@ class PlotDataItem(GraphicsObject):
|
|||||||
Set the downsampling mode of this item. Downsampling reduces the number
|
Set the downsampling mode of this item. Downsampling reduces the number
|
||||||
of samples drawn to increase performance.
|
of samples drawn to increase performance.
|
||||||
|
|
||||||
=========== =================================================================
|
============== =================================================================
|
||||||
Arguments
|
**Arguments:**
|
||||||
ds (int) Reduce visible plot samples by this factor. To disable,
|
ds (int) Reduce visible plot samples by this factor. To disable,
|
||||||
set ds=1.
|
set ds=1.
|
||||||
auto (bool) If True, automatically pick *ds* based on visible range
|
auto (bool) If True, automatically pick *ds* based on visible range
|
||||||
mode 'subsample': Downsample by taking the first of N samples.
|
mode 'subsample': Downsample by taking the first of N samples.
|
||||||
This method is fastest and least accurate.
|
This method is fastest and least accurate.
|
||||||
'mean': Downsample by taking the mean of N samples.
|
'mean': Downsample by taking the mean of N samples.
|
||||||
'peak': Downsample by drawing a saw wave that follows the min
|
'peak': Downsample by drawing a saw wave that follows the min
|
||||||
and max of the original data. This method produces the best
|
and max of the original data. This method produces the best
|
||||||
visual representation of the data but is slower.
|
visual representation of the data but is slower.
|
||||||
=========== =================================================================
|
============== =================================================================
|
||||||
"""
|
"""
|
||||||
changed = False
|
changed = False
|
||||||
if ds is not None:
|
if ds is not None:
|
||||||
@ -451,7 +461,7 @@ class PlotDataItem(GraphicsObject):
|
|||||||
def updateItems(self):
|
def updateItems(self):
|
||||||
|
|
||||||
curveArgs = {}
|
curveArgs = {}
|
||||||
for k,v in [('pen','pen'), ('shadowPen','shadowPen'), ('fillLevel','fillLevel'), ('fillBrush', 'brush'), ('antialias', 'antialias'), ('connect', 'connect')]:
|
for k,v in [('pen','pen'), ('shadowPen','shadowPen'), ('fillLevel','fillLevel'), ('fillBrush', 'brush'), ('antialias', 'antialias'), ('connect', 'connect'), ('stepMode', 'stepMode')]:
|
||||||
curveArgs[v] = self.opts[k]
|
curveArgs[v] = self.opts[k]
|
||||||
|
|
||||||
scatterArgs = {}
|
scatterArgs = {}
|
||||||
@ -527,7 +537,8 @@ class PlotDataItem(GraphicsObject):
|
|||||||
x0 = (range.left()-x[0]) / dx
|
x0 = (range.left()-x[0]) / dx
|
||||||
x1 = (range.right()-x[0]) / dx
|
x1 = (range.right()-x[0]) / dx
|
||||||
width = self.getViewBox().width()
|
width = self.getViewBox().width()
|
||||||
ds = int(max(1, int(0.2 * (x1-x0) / width)))
|
if width != 0.0:
|
||||||
|
ds = int(max(1, int(0.2 * (x1-x0) / width)))
|
||||||
## downsampling is expensive; delay until after clipping.
|
## downsampling is expensive; delay until after clipping.
|
||||||
|
|
||||||
if self.opts['clipToView']:
|
if self.opts['clipToView']:
|
||||||
@ -646,13 +657,12 @@ class PlotDataItem(GraphicsObject):
|
|||||||
|
|
||||||
def _fourierTransform(self, x, y):
|
def _fourierTransform(self, x, y):
|
||||||
## Perform fourier transform. If x values are not sampled uniformly,
|
## Perform fourier transform. If x values are not sampled uniformly,
|
||||||
## then use interpolate.griddata to resample before taking fft.
|
## then use np.interp to resample before taking fft.
|
||||||
dx = np.diff(x)
|
dx = np.diff(x)
|
||||||
uniform = not np.any(np.abs(dx-dx[0]) > (abs(dx[0]) / 1000.))
|
uniform = not np.any(np.abs(dx-dx[0]) > (abs(dx[0]) / 1000.))
|
||||||
if not uniform:
|
if not uniform:
|
||||||
import scipy.interpolate as interp
|
|
||||||
x2 = np.linspace(x[0], x[-1], len(x))
|
x2 = np.linspace(x[0], x[-1], len(x))
|
||||||
y = interp.griddata(x, y, x2, method='linear')
|
y = np.interp(x2, x, y)
|
||||||
x = x2
|
x = x2
|
||||||
f = np.fft.fft(y) / len(y)
|
f = np.fft.fft(y) / len(y)
|
||||||
y = abs(f[1:len(f)/2])
|
y = abs(f[1:len(f)/2])
|
||||||
|
@ -18,6 +18,7 @@ This class is very heavily featured:
|
|||||||
"""
|
"""
|
||||||
from ...Qt import QtGui, QtCore, QtSvg, USE_PYSIDE
|
from ...Qt import QtGui, QtCore, QtSvg, USE_PYSIDE
|
||||||
from ... import pixmaps
|
from ... import pixmaps
|
||||||
|
import sys
|
||||||
|
|
||||||
if USE_PYSIDE:
|
if USE_PYSIDE:
|
||||||
from .plotConfigTemplate_pyside import *
|
from .plotConfigTemplate_pyside import *
|
||||||
@ -69,6 +70,7 @@ class PlotItem(GraphicsWidget):
|
|||||||
:func:`setYLink <pyqtgraph.ViewBox.setYLink>`,
|
:func:`setYLink <pyqtgraph.ViewBox.setYLink>`,
|
||||||
:func:`setAutoPan <pyqtgraph.ViewBox.setAutoPan>`,
|
:func:`setAutoPan <pyqtgraph.ViewBox.setAutoPan>`,
|
||||||
:func:`setAutoVisible <pyqtgraph.ViewBox.setAutoVisible>`,
|
:func:`setAutoVisible <pyqtgraph.ViewBox.setAutoVisible>`,
|
||||||
|
:func:`setLimits <pyqtgraph.ViewBox.setLimits>`,
|
||||||
:func:`viewRect <pyqtgraph.ViewBox.viewRect>`,
|
:func:`viewRect <pyqtgraph.ViewBox.viewRect>`,
|
||||||
:func:`viewRange <pyqtgraph.ViewBox.viewRange>`,
|
:func:`viewRange <pyqtgraph.ViewBox.viewRange>`,
|
||||||
:func:`setMouseEnabled <pyqtgraph.ViewBox.setMouseEnabled>`,
|
:func:`setMouseEnabled <pyqtgraph.ViewBox.setMouseEnabled>`,
|
||||||
@ -82,7 +84,7 @@ class PlotItem(GraphicsWidget):
|
|||||||
The ViewBox itself can be accessed by calling :func:`getViewBox() <pyqtgraph.PlotItem.getViewBox>`
|
The ViewBox itself can be accessed by calling :func:`getViewBox() <pyqtgraph.PlotItem.getViewBox>`
|
||||||
|
|
||||||
==================== =======================================================================
|
==================== =======================================================================
|
||||||
**Signals**
|
**Signals:**
|
||||||
sigYRangeChanged wrapped from :class:`ViewBox <pyqtgraph.ViewBox>`
|
sigYRangeChanged wrapped from :class:`ViewBox <pyqtgraph.ViewBox>`
|
||||||
sigXRangeChanged wrapped from :class:`ViewBox <pyqtgraph.ViewBox>`
|
sigXRangeChanged wrapped from :class:`ViewBox <pyqtgraph.ViewBox>`
|
||||||
sigRangeChanged wrapped from :class:`ViewBox <pyqtgraph.ViewBox>`
|
sigRangeChanged wrapped from :class:`ViewBox <pyqtgraph.ViewBox>`
|
||||||
@ -102,7 +104,7 @@ class PlotItem(GraphicsWidget):
|
|||||||
Any extra keyword arguments are passed to PlotItem.plot().
|
Any extra keyword arguments are passed to PlotItem.plot().
|
||||||
|
|
||||||
============== ==========================================================================================
|
============== ==========================================================================================
|
||||||
**Arguments**
|
**Arguments:**
|
||||||
*title* Title to display at the top of the item. Html is allowed.
|
*title* Title to display at the top of the item. Html is allowed.
|
||||||
*labels* A dictionary specifying the axis labels to display::
|
*labels* A dictionary specifying the axis labels to display::
|
||||||
|
|
||||||
@ -192,14 +194,6 @@ class PlotItem(GraphicsWidget):
|
|||||||
self.layout.setColumnStretchFactor(1, 100)
|
self.layout.setColumnStretchFactor(1, 100)
|
||||||
|
|
||||||
|
|
||||||
## Wrap a few methods from viewBox
|
|
||||||
for m in [
|
|
||||||
'setXRange', 'setYRange', 'setXLink', 'setYLink', 'setAutoPan', 'setAutoVisible',
|
|
||||||
'setRange', 'autoRange', 'viewRect', 'viewRange', 'setMouseEnabled',
|
|
||||||
'enableAutoRange', 'disableAutoRange', 'setAspectLocked', 'invertY',
|
|
||||||
'register', 'unregister']: ## NOTE: If you update this list, please update the class docstring as well.
|
|
||||||
setattr(self, m, getattr(self.vb, m))
|
|
||||||
|
|
||||||
self.items = []
|
self.items = []
|
||||||
self.curves = []
|
self.curves = []
|
||||||
self.itemMeta = weakref.WeakKeyDictionary()
|
self.itemMeta = weakref.WeakKeyDictionary()
|
||||||
@ -296,7 +290,26 @@ class PlotItem(GraphicsWidget):
|
|||||||
def getViewBox(self):
|
def getViewBox(self):
|
||||||
"""Return the :class:`ViewBox <pyqtgraph.ViewBox>` contained within."""
|
"""Return the :class:`ViewBox <pyqtgraph.ViewBox>` contained within."""
|
||||||
return self.vb
|
return self.vb
|
||||||
|
|
||||||
|
|
||||||
|
## Wrap a few methods from viewBox.
|
||||||
|
#Important: don't use a settattr(m, getattr(self.vb, m)) as we'd be leaving the viebox alive
|
||||||
|
#because we had a reference to an instance method (creating wrapper methods at runtime instead).
|
||||||
|
|
||||||
|
for m in ['setXRange', 'setYRange', 'setXLink', 'setYLink', 'setAutoPan', # NOTE:
|
||||||
|
'setAutoVisible', 'setRange', 'autoRange', 'viewRect', 'viewRange', # If you update this list, please
|
||||||
|
'setMouseEnabled', 'setLimits', 'enableAutoRange', 'disableAutoRange', # update the class docstring
|
||||||
|
'setAspectLocked', 'invertY', 'register', 'unregister']: # as well.
|
||||||
|
|
||||||
|
def _create_method(name):
|
||||||
|
def method(self, *args, **kwargs):
|
||||||
|
return getattr(self.vb, name)(*args, **kwargs)
|
||||||
|
method.__name__ = name
|
||||||
|
return method
|
||||||
|
|
||||||
|
locals()[m] = _create_method(m)
|
||||||
|
|
||||||
|
del _create_method
|
||||||
|
|
||||||
|
|
||||||
def setLogMode(self, x=None, y=None):
|
def setLogMode(self, x=None, y=None):
|
||||||
@ -355,10 +368,8 @@ class PlotItem(GraphicsWidget):
|
|||||||
self.ctrlMenu.setParent(None)
|
self.ctrlMenu.setParent(None)
|
||||||
self.ctrlMenu = None
|
self.ctrlMenu = None
|
||||||
|
|
||||||
#self.ctrlBtn.setParent(None)
|
self.autoBtn.setParent(None)
|
||||||
#self.ctrlBtn = None
|
self.autoBtn = None
|
||||||
#self.autoBtn.setParent(None)
|
|
||||||
#self.autoBtn = None
|
|
||||||
|
|
||||||
for k in self.axes:
|
for k in self.axes:
|
||||||
i = self.axes[k]['item']
|
i = self.axes[k]['item']
|
||||||
@ -930,18 +941,18 @@ class PlotItem(GraphicsWidget):
|
|||||||
def setDownsampling(self, ds=None, auto=None, mode=None):
|
def setDownsampling(self, ds=None, auto=None, mode=None):
|
||||||
"""Change the default downsampling mode for all PlotDataItems managed by this plot.
|
"""Change the default downsampling mode for all PlotDataItems managed by this plot.
|
||||||
|
|
||||||
=========== =================================================================
|
=============== =================================================================
|
||||||
Arguments
|
**Arguments:**
|
||||||
ds (int) Reduce visible plot samples by this factor, or
|
ds (int) Reduce visible plot samples by this factor, or
|
||||||
(bool) To enable/disable downsampling without changing the value.
|
(bool) To enable/disable downsampling without changing the value.
|
||||||
auto (bool) If True, automatically pick *ds* based on visible range
|
auto (bool) If True, automatically pick *ds* based on visible range
|
||||||
mode 'subsample': Downsample by taking the first of N samples.
|
mode 'subsample': Downsample by taking the first of N samples.
|
||||||
This method is fastest and least accurate.
|
This method is fastest and least accurate.
|
||||||
'mean': Downsample by taking the mean of N samples.
|
'mean': Downsample by taking the mean of N samples.
|
||||||
'peak': Downsample by drawing a saw wave that follows the min
|
'peak': Downsample by drawing a saw wave that follows the min
|
||||||
and max of the original data. This method produces the best
|
and max of the original data. This method produces the best
|
||||||
visual representation of the data but is slower.
|
visual representation of the data but is slower.
|
||||||
=========== =================================================================
|
=============== =================================================================
|
||||||
"""
|
"""
|
||||||
if ds is not None:
|
if ds is not None:
|
||||||
if ds is False:
|
if ds is False:
|
||||||
@ -1112,15 +1123,15 @@ class PlotItem(GraphicsWidget):
|
|||||||
"""
|
"""
|
||||||
Set the label for an axis. Basic HTML formatting is allowed.
|
Set the label for an axis. Basic HTML formatting is allowed.
|
||||||
|
|
||||||
============= =================================================================
|
============== =================================================================
|
||||||
**Arguments**
|
**Arguments:**
|
||||||
axis must be one of 'left', 'bottom', 'right', or 'top'
|
axis must be one of 'left', 'bottom', 'right', or 'top'
|
||||||
text text to display along the axis. HTML allowed.
|
text text to display along the axis. HTML allowed.
|
||||||
units units to display after the title. If units are given,
|
units units to display after the title. If units are given,
|
||||||
then an SI prefix will be automatically appended
|
then an SI prefix will be automatically appended
|
||||||
and the axis values will be scaled accordingly.
|
and the axis values will be scaled accordingly.
|
||||||
(ie, use 'V' instead of 'mV'; 'm' will be added automatically)
|
(ie, use 'V' instead of 'mV'; 'm' will be added automatically)
|
||||||
============= =================================================================
|
============== =================================================================
|
||||||
"""
|
"""
|
||||||
self.getAxis(axis).setLabel(text=text, units=units, **args)
|
self.getAxis(axis).setLabel(text=text, units=units, **args)
|
||||||
self.showAxis(axis)
|
self.showAxis(axis)
|
||||||
|
@ -13,11 +13,8 @@ of how to build an ROI at the bottom of the file.
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
from ..Qt import QtCore, QtGui
|
from ..Qt import QtCore, QtGui
|
||||||
#if not hasattr(QtCore, 'Signal'):
|
|
||||||
#QtCore.Signal = QtCore.pyqtSignal
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
from numpy.linalg import norm
|
#from numpy.linalg import norm
|
||||||
import scipy.ndimage as ndimage
|
|
||||||
from ..Point import *
|
from ..Point import *
|
||||||
from ..SRTTransform import SRTTransform
|
from ..SRTTransform import SRTTransform
|
||||||
from math import cos, sin
|
from math import cos, sin
|
||||||
@ -36,11 +33,56 @@ def rectStr(r):
|
|||||||
return "[%f, %f] + [%f, %f]" % (r.x(), r.y(), r.width(), r.height())
|
return "[%f, %f] + [%f, %f]" % (r.x(), r.y(), r.width(), r.height())
|
||||||
|
|
||||||
class ROI(GraphicsObject):
|
class ROI(GraphicsObject):
|
||||||
"""Generic region-of-interest widget.
|
"""
|
||||||
Can be used for implementing many types of selection box with rotate/translate/scale handles.
|
Generic region-of-interest widget.
|
||||||
|
|
||||||
Signals
|
Can be used for implementing many types of selection box with
|
||||||
----------------------- ----------------------------------------------------
|
rotate/translate/scale handles.
|
||||||
|
ROIs can be customized to have a variety of shapes (by subclassing or using
|
||||||
|
any of the built-in subclasses) and any combination of draggable handles
|
||||||
|
that allow the user to manipulate the ROI.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
================ ===========================================================
|
||||||
|
**Arguments**
|
||||||
|
pos (length-2 sequence) Indicates the position of the ROI's
|
||||||
|
origin. For most ROIs, this is the lower-left corner of
|
||||||
|
its bounding rectangle.
|
||||||
|
size (length-2 sequence) Indicates the width and height of the
|
||||||
|
ROI.
|
||||||
|
angle (float) The rotation of the ROI in degrees. Default is 0.
|
||||||
|
invertible (bool) If True, the user may resize the ROI to have
|
||||||
|
negative width or height (assuming the ROI has scale
|
||||||
|
handles). Default is False.
|
||||||
|
maxBounds (QRect, QRectF, or None) Specifies boundaries that the ROI
|
||||||
|
cannot be dragged outside of by the user. Default is None.
|
||||||
|
snapSize (float) The spacing of snap positions used when *scaleSnap*
|
||||||
|
or *translateSnap* are enabled. Default is 1.0.
|
||||||
|
scaleSnap (bool) If True, the width and height of the ROI are forced
|
||||||
|
to be integer multiples of *snapSize* when being resized
|
||||||
|
by the user. Default is False.
|
||||||
|
translateSnap (bool) If True, the x and y positions of the ROI are forced
|
||||||
|
to be integer multiples of *snapSize* when being resized
|
||||||
|
by the user. Default is False.
|
||||||
|
rotateSnap (bool) If True, the ROI angle is forced to a multiple of
|
||||||
|
15 degrees when rotated by the user. Default is False.
|
||||||
|
parent (QGraphicsItem) The graphics item parent of this ROI. It
|
||||||
|
is generally not necessary to specify the parent.
|
||||||
|
pen (QPen or argument to pg.mkPen) The pen to use when drawing
|
||||||
|
the shape of the ROI.
|
||||||
|
movable (bool) If True, the ROI can be moved by dragging anywhere
|
||||||
|
inside the ROI. Default is True.
|
||||||
|
removable (bool) If True, the ROI will be given a context menu with
|
||||||
|
an option to remove the ROI. The ROI emits
|
||||||
|
sigRemoveRequested when this menu action is selected.
|
||||||
|
Default is False.
|
||||||
|
================ ===========================================================
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
======================= ====================================================
|
||||||
|
**Signals**
|
||||||
sigRegionChangeFinished Emitted when the user stops dragging the ROI (or
|
sigRegionChangeFinished Emitted when the user stops dragging the ROI (or
|
||||||
one of its handles) or if the ROI is changed
|
one of its handles) or if the ROI is changed
|
||||||
programatically.
|
programatically.
|
||||||
@ -58,7 +100,7 @@ class ROI(GraphicsObject):
|
|||||||
details.
|
details.
|
||||||
sigRemoveRequested Emitted when the user selects 'remove' from the
|
sigRemoveRequested Emitted when the user selects 'remove' from the
|
||||||
ROI's context menu (if available).
|
ROI's context menu (if available).
|
||||||
----------------------- ----------------------------------------------------
|
======================= ====================================================
|
||||||
"""
|
"""
|
||||||
|
|
||||||
sigRegionChangeFinished = QtCore.Signal(object)
|
sigRegionChangeFinished = QtCore.Signal(object)
|
||||||
@ -117,7 +159,11 @@ class ROI(GraphicsObject):
|
|||||||
return sc
|
return sc
|
||||||
|
|
||||||
def saveState(self):
|
def saveState(self):
|
||||||
"""Return the state of the widget in a format suitable for storing to disk. (Points are converted to tuple)"""
|
"""Return the state of the widget in a format suitable for storing to
|
||||||
|
disk. (Points are converted to tuple)
|
||||||
|
|
||||||
|
Combined with setState(), this allows ROIs to be easily saved and
|
||||||
|
restored."""
|
||||||
state = {}
|
state = {}
|
||||||
state['pos'] = tuple(self.state['pos'])
|
state['pos'] = tuple(self.state['pos'])
|
||||||
state['size'] = tuple(self.state['size'])
|
state['size'] = tuple(self.state['size'])
|
||||||
@ -125,6 +171,10 @@ class ROI(GraphicsObject):
|
|||||||
return state
|
return state
|
||||||
|
|
||||||
def setState(self, state, update=True):
|
def setState(self, state, update=True):
|
||||||
|
"""
|
||||||
|
Set the state of the ROI from a structure generated by saveState() or
|
||||||
|
getState().
|
||||||
|
"""
|
||||||
self.setPos(state['pos'], update=False)
|
self.setPos(state['pos'], update=False)
|
||||||
self.setSize(state['size'], update=False)
|
self.setSize(state['size'], update=False)
|
||||||
self.setAngle(state['angle'], update=update)
|
self.setAngle(state['angle'], update=update)
|
||||||
@ -135,20 +185,32 @@ class ROI(GraphicsObject):
|
|||||||
h['item'].setZValue(z+1)
|
h['item'].setZValue(z+1)
|
||||||
|
|
||||||
def parentBounds(self):
|
def parentBounds(self):
|
||||||
|
"""
|
||||||
|
Return the bounding rectangle of this ROI in the coordinate system
|
||||||
|
of its parent.
|
||||||
|
"""
|
||||||
return self.mapToParent(self.boundingRect()).boundingRect()
|
return self.mapToParent(self.boundingRect()).boundingRect()
|
||||||
|
|
||||||
def setPen(self, pen):
|
def setPen(self, *args, **kwargs):
|
||||||
self.pen = fn.mkPen(pen)
|
"""
|
||||||
|
Set the pen to use when drawing the ROI shape.
|
||||||
|
For arguments, see :func:`mkPen <pyqtgraph.mkPen>`.
|
||||||
|
"""
|
||||||
|
self.pen = fn.mkPen(*args, **kwargs)
|
||||||
self.currentPen = self.pen
|
self.currentPen = self.pen
|
||||||
self.update()
|
self.update()
|
||||||
|
|
||||||
def size(self):
|
def size(self):
|
||||||
|
"""Return the size (w,h) of the ROI."""
|
||||||
return self.getState()['size']
|
return self.getState()['size']
|
||||||
|
|
||||||
def pos(self):
|
def pos(self):
|
||||||
|
"""Return the position (x,y) of the ROI's origin.
|
||||||
|
For most ROIs, this will be the lower-left corner."""
|
||||||
return self.getState()['pos']
|
return self.getState()['pos']
|
||||||
|
|
||||||
def angle(self):
|
def angle(self):
|
||||||
|
"""Return the angle of the ROI in degrees."""
|
||||||
return self.getState()['angle']
|
return self.getState()['angle']
|
||||||
|
|
||||||
def setPos(self, pos, update=True, finish=True):
|
def setPos(self, pos, update=True, finish=True):
|
||||||
@ -214,11 +276,14 @@ class ROI(GraphicsObject):
|
|||||||
If the ROI is bounded and the move would exceed boundaries, then the ROI
|
If the ROI is bounded and the move would exceed boundaries, then the ROI
|
||||||
is moved to the nearest acceptable position instead.
|
is moved to the nearest acceptable position instead.
|
||||||
|
|
||||||
snap can be:
|
*snap* can be:
|
||||||
None (default): use self.translateSnap and self.snapSize to determine whether/how to snap
|
|
||||||
False: do not snap
|
=============== ==========================================================================
|
||||||
Point(w,h) snap to rectangular grid with spacing (w,h)
|
None (default) use self.translateSnap and self.snapSize to determine whether/how to snap
|
||||||
True: snap using self.snapSize (and ignoring self.translateSnap)
|
False do not snap
|
||||||
|
Point(w,h) snap to rectangular grid with spacing (w,h)
|
||||||
|
True snap using self.snapSize (and ignoring self.translateSnap)
|
||||||
|
=============== ==========================================================================
|
||||||
|
|
||||||
Also accepts *update* and *finish* arguments (see setPos() for a description of these).
|
Also accepts *update* and *finish* arguments (see setPos() for a description of these).
|
||||||
"""
|
"""
|
||||||
@ -264,21 +329,86 @@ class ROI(GraphicsObject):
|
|||||||
#self.stateChanged()
|
#self.stateChanged()
|
||||||
|
|
||||||
def rotate(self, angle, update=True, finish=True):
|
def rotate(self, angle, update=True, finish=True):
|
||||||
|
"""
|
||||||
|
Rotate the ROI by *angle* degrees.
|
||||||
|
|
||||||
|
Also accepts *update* and *finish* arguments (see setPos() for a
|
||||||
|
description of these).
|
||||||
|
"""
|
||||||
self.setAngle(self.angle()+angle, update=update, finish=finish)
|
self.setAngle(self.angle()+angle, update=update, finish=finish)
|
||||||
|
|
||||||
def handleMoveStarted(self):
|
def handleMoveStarted(self):
|
||||||
self.preMoveState = self.getState()
|
self.preMoveState = self.getState()
|
||||||
|
|
||||||
def addTranslateHandle(self, pos, axes=None, item=None, name=None, index=None):
|
def addTranslateHandle(self, pos, axes=None, item=None, name=None, index=None):
|
||||||
|
"""
|
||||||
|
Add a new translation handle to the ROI. Dragging the handle will move
|
||||||
|
the entire ROI without changing its angle or shape.
|
||||||
|
|
||||||
|
Note that, by default, ROIs may be moved by dragging anywhere inside the
|
||||||
|
ROI. However, for larger ROIs it may be desirable to disable this and
|
||||||
|
instead provide one or more translation handles.
|
||||||
|
|
||||||
|
=================== ====================================================
|
||||||
|
**Arguments**
|
||||||
|
pos (length-2 sequence) The position of the handle
|
||||||
|
relative to the shape of the ROI. A value of (0,0)
|
||||||
|
indicates the origin, whereas (1, 1) indicates the
|
||||||
|
upper-right corner, regardless of the ROI's size.
|
||||||
|
item The Handle instance to add. If None, a new handle
|
||||||
|
will be created.
|
||||||
|
name The name of this handle (optional). Handles are
|
||||||
|
identified by name when calling
|
||||||
|
getLocalHandlePositions and getSceneHandlePositions.
|
||||||
|
=================== ====================================================
|
||||||
|
"""
|
||||||
pos = Point(pos)
|
pos = Point(pos)
|
||||||
return self.addHandle({'name': name, 'type': 't', 'pos': pos, 'item': item}, index=index)
|
return self.addHandle({'name': name, 'type': 't', 'pos': pos, 'item': item}, index=index)
|
||||||
|
|
||||||
def addFreeHandle(self, pos=None, axes=None, item=None, name=None, index=None):
|
def addFreeHandle(self, pos=None, axes=None, item=None, name=None, index=None):
|
||||||
|
"""
|
||||||
|
Add a new free handle to the ROI. Dragging free handles has no effect
|
||||||
|
on the position or shape of the ROI.
|
||||||
|
|
||||||
|
=================== ====================================================
|
||||||
|
**Arguments**
|
||||||
|
pos (length-2 sequence) The position of the handle
|
||||||
|
relative to the shape of the ROI. A value of (0,0)
|
||||||
|
indicates the origin, whereas (1, 1) indicates the
|
||||||
|
upper-right corner, regardless of the ROI's size.
|
||||||
|
item The Handle instance to add. If None, a new handle
|
||||||
|
will be created.
|
||||||
|
name The name of this handle (optional). Handles are
|
||||||
|
identified by name when calling
|
||||||
|
getLocalHandlePositions and getSceneHandlePositions.
|
||||||
|
=================== ====================================================
|
||||||
|
"""
|
||||||
if pos is not None:
|
if pos is not None:
|
||||||
pos = Point(pos)
|
pos = Point(pos)
|
||||||
return self.addHandle({'name': name, 'type': 'f', 'pos': pos, 'item': item}, index=index)
|
return self.addHandle({'name': name, 'type': 'f', 'pos': pos, 'item': item}, index=index)
|
||||||
|
|
||||||
def addScaleHandle(self, pos, center, axes=None, item=None, name=None, lockAspect=False, index=None):
|
def addScaleHandle(self, pos, center, axes=None, item=None, name=None, lockAspect=False, index=None):
|
||||||
|
"""
|
||||||
|
Add a new scale handle to the ROI. Dragging a scale handle allows the
|
||||||
|
user to change the height and/or width of the ROI.
|
||||||
|
|
||||||
|
=================== ====================================================
|
||||||
|
**Arguments**
|
||||||
|
pos (length-2 sequence) The position of the handle
|
||||||
|
relative to the shape of the ROI. A value of (0,0)
|
||||||
|
indicates the origin, whereas (1, 1) indicates the
|
||||||
|
upper-right corner, regardless of the ROI's size.
|
||||||
|
center (length-2 sequence) The center point around which
|
||||||
|
scaling takes place. If the center point has the
|
||||||
|
same x or y value as the handle position, then
|
||||||
|
scaling will be disabled for that axis.
|
||||||
|
item The Handle instance to add. If None, a new handle
|
||||||
|
will be created.
|
||||||
|
name The name of this handle (optional). Handles are
|
||||||
|
identified by name when calling
|
||||||
|
getLocalHandlePositions and getSceneHandlePositions.
|
||||||
|
=================== ====================================================
|
||||||
|
"""
|
||||||
pos = Point(pos)
|
pos = Point(pos)
|
||||||
center = Point(center)
|
center = Point(center)
|
||||||
info = {'name': name, 'type': 's', 'center': center, 'pos': pos, 'item': item, 'lockAspect': lockAspect}
|
info = {'name': name, 'type': 's', 'center': center, 'pos': pos, 'item': item, 'lockAspect': lockAspect}
|
||||||
@ -289,11 +419,51 @@ class ROI(GraphicsObject):
|
|||||||
return self.addHandle(info, index=index)
|
return self.addHandle(info, index=index)
|
||||||
|
|
||||||
def addRotateHandle(self, pos, center, item=None, name=None, index=None):
|
def addRotateHandle(self, pos, center, item=None, name=None, index=None):
|
||||||
|
"""
|
||||||
|
Add a new rotation handle to the ROI. Dragging a rotation handle allows
|
||||||
|
the user to change the angle of the ROI.
|
||||||
|
|
||||||
|
=================== ====================================================
|
||||||
|
**Arguments**
|
||||||
|
pos (length-2 sequence) The position of the handle
|
||||||
|
relative to the shape of the ROI. A value of (0,0)
|
||||||
|
indicates the origin, whereas (1, 1) indicates the
|
||||||
|
upper-right corner, regardless of the ROI's size.
|
||||||
|
center (length-2 sequence) The center point around which
|
||||||
|
rotation takes place.
|
||||||
|
item The Handle instance to add. If None, a new handle
|
||||||
|
will be created.
|
||||||
|
name The name of this handle (optional). Handles are
|
||||||
|
identified by name when calling
|
||||||
|
getLocalHandlePositions and getSceneHandlePositions.
|
||||||
|
=================== ====================================================
|
||||||
|
"""
|
||||||
pos = Point(pos)
|
pos = Point(pos)
|
||||||
center = Point(center)
|
center = Point(center)
|
||||||
return self.addHandle({'name': name, 'type': 'r', 'center': center, 'pos': pos, 'item': item}, index=index)
|
return self.addHandle({'name': name, 'type': 'r', 'center': center, 'pos': pos, 'item': item}, index=index)
|
||||||
|
|
||||||
def addScaleRotateHandle(self, pos, center, item=None, name=None, index=None):
|
def addScaleRotateHandle(self, pos, center, item=None, name=None, index=None):
|
||||||
|
"""
|
||||||
|
Add a new scale+rotation handle to the ROI. When dragging a handle of
|
||||||
|
this type, the user can simultaneously rotate the ROI around an
|
||||||
|
arbitrary center point as well as scale the ROI by dragging the handle
|
||||||
|
toward or away from the center point.
|
||||||
|
|
||||||
|
=================== ====================================================
|
||||||
|
**Arguments**
|
||||||
|
pos (length-2 sequence) The position of the handle
|
||||||
|
relative to the shape of the ROI. A value of (0,0)
|
||||||
|
indicates the origin, whereas (1, 1) indicates the
|
||||||
|
upper-right corner, regardless of the ROI's size.
|
||||||
|
center (length-2 sequence) The center point around which
|
||||||
|
scaling and rotation take place.
|
||||||
|
item The Handle instance to add. If None, a new handle
|
||||||
|
will be created.
|
||||||
|
name The name of this handle (optional). Handles are
|
||||||
|
identified by name when calling
|
||||||
|
getLocalHandlePositions and getSceneHandlePositions.
|
||||||
|
=================== ====================================================
|
||||||
|
"""
|
||||||
pos = Point(pos)
|
pos = Point(pos)
|
||||||
center = Point(center)
|
center = Point(center)
|
||||||
if pos[0] != center[0] and pos[1] != center[1]:
|
if pos[0] != center[0] and pos[1] != center[1]:
|
||||||
@ -301,6 +471,27 @@ class ROI(GraphicsObject):
|
|||||||
return self.addHandle({'name': name, 'type': 'sr', 'center': center, 'pos': pos, 'item': item}, index=index)
|
return self.addHandle({'name': name, 'type': 'sr', 'center': center, 'pos': pos, 'item': item}, index=index)
|
||||||
|
|
||||||
def addRotateFreeHandle(self, pos, center, axes=None, item=None, name=None, index=None):
|
def addRotateFreeHandle(self, pos, center, axes=None, item=None, name=None, index=None):
|
||||||
|
"""
|
||||||
|
Add a new rotation+free handle to the ROI. When dragging a handle of
|
||||||
|
this type, the user can rotate the ROI around an
|
||||||
|
arbitrary center point, while moving toward or away from the center
|
||||||
|
point has no effect on the shape of the ROI.
|
||||||
|
|
||||||
|
=================== ====================================================
|
||||||
|
**Arguments**
|
||||||
|
pos (length-2 sequence) The position of the handle
|
||||||
|
relative to the shape of the ROI. A value of (0,0)
|
||||||
|
indicates the origin, whereas (1, 1) indicates the
|
||||||
|
upper-right corner, regardless of the ROI's size.
|
||||||
|
center (length-2 sequence) The center point around which
|
||||||
|
rotation takes place.
|
||||||
|
item The Handle instance to add. If None, a new handle
|
||||||
|
will be created.
|
||||||
|
name The name of this handle (optional). Handles are
|
||||||
|
identified by name when calling
|
||||||
|
getLocalHandlePositions and getSceneHandlePositions.
|
||||||
|
=================== ====================================================
|
||||||
|
"""
|
||||||
pos = Point(pos)
|
pos = Point(pos)
|
||||||
center = Point(center)
|
center = Point(center)
|
||||||
return self.addHandle({'name': name, 'type': 'rf', 'center': center, 'pos': pos, 'item': item}, index=index)
|
return self.addHandle({'name': name, 'type': 'rf', 'center': center, 'pos': pos, 'item': item}, index=index)
|
||||||
@ -329,6 +520,9 @@ class ROI(GraphicsObject):
|
|||||||
return h
|
return h
|
||||||
|
|
||||||
def indexOfHandle(self, handle):
|
def indexOfHandle(self, handle):
|
||||||
|
"""
|
||||||
|
Return the index of *handle* in the list of this ROI's handles.
|
||||||
|
"""
|
||||||
if isinstance(handle, Handle):
|
if isinstance(handle, Handle):
|
||||||
index = [i for i, info in enumerate(self.handles) if info['item'] is handle]
|
index = [i for i, info in enumerate(self.handles) if info['item'] is handle]
|
||||||
if len(index) == 0:
|
if len(index) == 0:
|
||||||
@ -338,7 +532,8 @@ class ROI(GraphicsObject):
|
|||||||
return handle
|
return handle
|
||||||
|
|
||||||
def removeHandle(self, handle):
|
def removeHandle(self, handle):
|
||||||
"""Remove a handle from this ROI. Argument may be either a Handle instance or the integer index of the handle."""
|
"""Remove a handle from this ROI. Argument may be either a Handle
|
||||||
|
instance or the integer index of the handle."""
|
||||||
index = self.indexOfHandle(handle)
|
index = self.indexOfHandle(handle)
|
||||||
|
|
||||||
handle = self.handles[index]['item']
|
handle = self.handles[index]['item']
|
||||||
@ -349,20 +544,17 @@ class ROI(GraphicsObject):
|
|||||||
self.stateChanged()
|
self.stateChanged()
|
||||||
|
|
||||||
def replaceHandle(self, oldHandle, newHandle):
|
def replaceHandle(self, oldHandle, newHandle):
|
||||||
"""Replace one handle in the ROI for another. This is useful when connecting multiple ROIs together.
|
"""Replace one handle in the ROI for another. This is useful when
|
||||||
*oldHandle* may be a Handle instance or the index of a handle."""
|
connecting multiple ROIs together.
|
||||||
#print "========================="
|
|
||||||
#print "replace", oldHandle, newHandle
|
*oldHandle* may be a Handle instance or the index of a handle to be
|
||||||
#print self
|
replaced."""
|
||||||
#print self.handles
|
|
||||||
#print "-----------------"
|
|
||||||
index = self.indexOfHandle(oldHandle)
|
index = self.indexOfHandle(oldHandle)
|
||||||
info = self.handles[index]
|
info = self.handles[index]
|
||||||
self.removeHandle(index)
|
self.removeHandle(index)
|
||||||
info['item'] = newHandle
|
info['item'] = newHandle
|
||||||
info['pos'] = newHandle.pos()
|
info['pos'] = newHandle.pos()
|
||||||
self.addHandle(info, index=index)
|
self.addHandle(info, index=index)
|
||||||
#print self.handles
|
|
||||||
|
|
||||||
def checkRemoveHandle(self, handle):
|
def checkRemoveHandle(self, handle):
|
||||||
## This is used when displaying a Handle's context menu to determine
|
## This is used when displaying a Handle's context menu to determine
|
||||||
@ -373,7 +565,10 @@ class ROI(GraphicsObject):
|
|||||||
|
|
||||||
|
|
||||||
def getLocalHandlePositions(self, index=None):
|
def getLocalHandlePositions(self, index=None):
|
||||||
"""Returns the position of a handle in ROI coordinates"""
|
"""Returns the position of handles in the ROI's coordinate system.
|
||||||
|
|
||||||
|
The format returned is a list of (name, pos) tuples.
|
||||||
|
"""
|
||||||
if index == None:
|
if index == None:
|
||||||
positions = []
|
positions = []
|
||||||
for h in self.handles:
|
for h in self.handles:
|
||||||
@ -383,6 +578,10 @@ class ROI(GraphicsObject):
|
|||||||
return (self.handles[index]['name'], self.handles[index]['pos'])
|
return (self.handles[index]['name'], self.handles[index]['pos'])
|
||||||
|
|
||||||
def getSceneHandlePositions(self, index=None):
|
def getSceneHandlePositions(self, index=None):
|
||||||
|
"""Returns the position of handles in the scene coordinate system.
|
||||||
|
|
||||||
|
The format returned is a list of (name, pos) tuples.
|
||||||
|
"""
|
||||||
if index == None:
|
if index == None:
|
||||||
positions = []
|
positions = []
|
||||||
for h in self.handles:
|
for h in self.handles:
|
||||||
@ -392,6 +591,9 @@ class ROI(GraphicsObject):
|
|||||||
return (self.handles[index]['name'], self.handles[index]['item'].scenePos())
|
return (self.handles[index]['name'], self.handles[index]['item'].scenePos())
|
||||||
|
|
||||||
def getHandles(self):
|
def getHandles(self):
|
||||||
|
"""
|
||||||
|
Return a list of this ROI's Handles.
|
||||||
|
"""
|
||||||
return [h['item'] for h in self.handles]
|
return [h['item'] for h in self.handles]
|
||||||
|
|
||||||
def mapSceneToParent(self, pt):
|
def mapSceneToParent(self, pt):
|
||||||
@ -463,12 +665,8 @@ class ROI(GraphicsObject):
|
|||||||
|
|
||||||
def removeClicked(self):
|
def removeClicked(self):
|
||||||
## Send remove event only after we have exited the menu event handler
|
## Send remove event only after we have exited the menu event handler
|
||||||
self.removeTimer = QtCore.QTimer()
|
QtCore.QTimer.singleShot(0, lambda: self.sigRemoveRequested.emit(self))
|
||||||
self.removeTimer.timeout.connect(lambda: self.sigRemoveRequested.emit(self))
|
|
||||||
self.removeTimer.start(0)
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def mouseDragEvent(self, ev):
|
def mouseDragEvent(self, ev):
|
||||||
if ev.isStart():
|
if ev.isStart():
|
||||||
#p = ev.pos()
|
#p = ev.pos()
|
||||||
@ -510,56 +708,16 @@ class ROI(GraphicsObject):
|
|||||||
self.sigClicked.emit(self, ev)
|
self.sigClicked.emit(self, ev)
|
||||||
else:
|
else:
|
||||||
ev.ignore()
|
ev.ignore()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def cancelMove(self):
|
def cancelMove(self):
|
||||||
self.isMoving = False
|
self.isMoving = False
|
||||||
self.setState(self.preMoveState)
|
self.setState(self.preMoveState)
|
||||||
|
|
||||||
|
|
||||||
#def pointDragEvent(self, pt, ev):
|
|
||||||
### just for handling drag start/stop.
|
|
||||||
### drag moves are handled through movePoint()
|
|
||||||
|
|
||||||
#if ev.isStart():
|
|
||||||
#self.isMoving = True
|
|
||||||
#self.preMoveState = self.getState()
|
|
||||||
|
|
||||||
#self.sigRegionChangeStarted.emit(self)
|
|
||||||
#elif ev.isFinish():
|
|
||||||
#self.isMoving = False
|
|
||||||
#self.sigRegionChangeFinished.emit(self)
|
|
||||||
#return
|
|
||||||
|
|
||||||
|
|
||||||
#def pointPressEvent(self, pt, ev):
|
|
||||||
##print "press"
|
|
||||||
#self.isMoving = True
|
|
||||||
#self.preMoveState = self.getState()
|
|
||||||
|
|
||||||
##self.emit(QtCore.SIGNAL('regionChangeStarted'), self)
|
|
||||||
#self.sigRegionChangeStarted.emit(self)
|
|
||||||
##self.pressPos = self.mapFromScene(ev.scenePos())
|
|
||||||
##self.pressHandlePos = self.handles[pt]['item'].pos()
|
|
||||||
|
|
||||||
#def pointReleaseEvent(self, pt, ev):
|
|
||||||
##print "release"
|
|
||||||
#self.isMoving = False
|
|
||||||
##self.emit(QtCore.SIGNAL('regionChangeFinished'), self)
|
|
||||||
#self.sigRegionChangeFinished.emit(self)
|
|
||||||
|
|
||||||
#def pointMoveEvent(self, pt, ev):
|
|
||||||
#self.movePoint(pt, ev.scenePos(), ev.modifiers())
|
|
||||||
|
|
||||||
|
|
||||||
def checkPointMove(self, handle, pos, modifiers):
|
def checkPointMove(self, handle, pos, modifiers):
|
||||||
"""When handles move, they must ask the ROI if the move is acceptable.
|
"""When handles move, they must ask the ROI if the move is acceptable.
|
||||||
By default, this always returns True. Subclasses may wish override.
|
By default, this always returns True. Subclasses may wish override.
|
||||||
"""
|
"""
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
def movePoint(self, handle, pos, modifiers=QtCore.Qt.KeyboardModifier(), finish=True, coords='parent'):
|
def movePoint(self, handle, pos, modifiers=QtCore.Qt.KeyboardModifier(), finish=True, coords='parent'):
|
||||||
## called by Handles when they are moved.
|
## called by Handles when they are moved.
|
||||||
@ -664,7 +822,10 @@ class ROI(GraphicsObject):
|
|||||||
if not self.rotateAllowed:
|
if not self.rotateAllowed:
|
||||||
return
|
return
|
||||||
## If the handle is directly over its center point, we can't compute an angle.
|
## If the handle is directly over its center point, we can't compute an angle.
|
||||||
if lp1.length() == 0 or lp0.length() == 0:
|
try:
|
||||||
|
if lp1.length() == 0 or lp0.length() == 0:
|
||||||
|
return
|
||||||
|
except OverflowError:
|
||||||
return
|
return
|
||||||
|
|
||||||
## determine new rotation angle, constrained if necessary
|
## determine new rotation angle, constrained if necessary
|
||||||
@ -704,7 +865,10 @@ class ROI(GraphicsObject):
|
|||||||
else:
|
else:
|
||||||
scaleAxis = 0
|
scaleAxis = 0
|
||||||
|
|
||||||
if lp1.length() == 0 or lp0.length() == 0:
|
try:
|
||||||
|
if lp1.length() == 0 or lp0.length() == 0:
|
||||||
|
return
|
||||||
|
except OverflowError:
|
||||||
return
|
return
|
||||||
|
|
||||||
ang = newState['angle'] - lp0.angle(lp1)
|
ang = newState['angle'] - lp0.angle(lp1)
|
||||||
@ -804,7 +968,6 @@ class ROI(GraphicsObject):
|
|||||||
round(pos[1] / snap[1]) * snap[1]
|
round(pos[1] / snap[1]) * snap[1]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def boundingRect(self):
|
def boundingRect(self):
|
||||||
return QtCore.QRectF(0, 0, self.state['size'][0], self.state['size'][1]).normalized()
|
return QtCore.QRectF(0, 0, self.state['size'][0], self.state['size'][1]).normalized()
|
||||||
|
|
||||||
@ -871,7 +1034,25 @@ class ROI(GraphicsObject):
|
|||||||
return bounds, tr
|
return bounds, tr
|
||||||
|
|
||||||
def getArrayRegion(self, data, img, axes=(0,1), returnMappedCoords=False, **kwds):
|
def getArrayRegion(self, data, img, axes=(0,1), returnMappedCoords=False, **kwds):
|
||||||
"""Use the position and orientation of this ROI relative to an imageItem to pull a slice from an array.
|
"""Use the position and orientation of this ROI relative to an imageItem
|
||||||
|
to pull a slice from an array.
|
||||||
|
|
||||||
|
=================== ====================================================
|
||||||
|
**Arguments**
|
||||||
|
data The array to slice from. Note that this array does
|
||||||
|
*not* have to be the same data that is represented
|
||||||
|
in *img*.
|
||||||
|
img (ImageItem or other suitable QGraphicsItem)
|
||||||
|
Used to determine the relationship between the
|
||||||
|
ROI and the boundaries of *data*.
|
||||||
|
axes (length-2 tuple) Specifies the axes in *data* that
|
||||||
|
correspond to the x and y axes of *img*.
|
||||||
|
returnMappedCoords (bool) If True, the array slice is returned along
|
||||||
|
with a corresponding array of coordinates that were
|
||||||
|
used to extract data from the original array.
|
||||||
|
\**kwds All keyword arguments are passed to
|
||||||
|
:func:`affineSlice <pyqtgraph.affineSlice>`.
|
||||||
|
=================== ====================================================
|
||||||
|
|
||||||
This method uses :func:`affineSlice <pyqtgraph.affineSlice>` to generate
|
This method uses :func:`affineSlice <pyqtgraph.affineSlice>` to generate
|
||||||
the slice from *data* and uses :func:`getAffineSliceParams <pyqtgraph.ROI.getAffineSliceParams>` to determine the parameters to
|
the slice from *data* and uses :func:`getAffineSliceParams <pyqtgraph.ROI.getAffineSliceParams>` to determine the parameters to
|
||||||
@ -905,105 +1086,6 @@ class ROI(GraphicsObject):
|
|||||||
#mapped += translate.reshape((2,1,1))
|
#mapped += translate.reshape((2,1,1))
|
||||||
mapped = fn.transformCoordinates(img.transform(), coords)
|
mapped = fn.transformCoordinates(img.transform(), coords)
|
||||||
return result, mapped
|
return result, mapped
|
||||||
|
|
||||||
|
|
||||||
### transpose data so x and y are the first 2 axes
|
|
||||||
#trAx = range(0, data.ndim)
|
|
||||||
#trAx.remove(axes[0])
|
|
||||||
#trAx.remove(axes[1])
|
|
||||||
#tr1 = tuple(axes) + tuple(trAx)
|
|
||||||
#arr = data.transpose(tr1)
|
|
||||||
|
|
||||||
### Determine the minimal area of the data we will need
|
|
||||||
#(dataBounds, roiDataTransform) = self.getArraySlice(data, img, returnSlice=False, axes=axes)
|
|
||||||
|
|
||||||
### Pad data boundaries by 1px if possible
|
|
||||||
#dataBounds = (
|
|
||||||
#(max(dataBounds[0][0]-1, 0), min(dataBounds[0][1]+1, arr.shape[0])),
|
|
||||||
#(max(dataBounds[1][0]-1, 0), min(dataBounds[1][1]+1, arr.shape[1]))
|
|
||||||
#)
|
|
||||||
|
|
||||||
### Extract minimal data from array
|
|
||||||
#arr1 = arr[dataBounds[0][0]:dataBounds[0][1], dataBounds[1][0]:dataBounds[1][1]]
|
|
||||||
|
|
||||||
### Update roiDataTransform to reflect this extraction
|
|
||||||
#roiDataTransform *= QtGui.QTransform().translate(-dataBounds[0][0], -dataBounds[1][0])
|
|
||||||
#### (roiDataTransform now maps from ROI coords to extracted data coords)
|
|
||||||
|
|
||||||
|
|
||||||
### Rotate array
|
|
||||||
#if abs(self.state['angle']) > 1e-5:
|
|
||||||
#arr2 = ndimage.rotate(arr1, self.state['angle'] * 180 / np.pi, order=1)
|
|
||||||
|
|
||||||
### update data transforms to reflect this rotation
|
|
||||||
#rot = QtGui.QTransform().rotate(self.state['angle'] * 180 / np.pi)
|
|
||||||
#roiDataTransform *= rot
|
|
||||||
|
|
||||||
### The rotation also causes a shift which must be accounted for:
|
|
||||||
#dataBound = QtCore.QRectF(0, 0, arr1.shape[0], arr1.shape[1])
|
|
||||||
#rotBound = rot.mapRect(dataBound)
|
|
||||||
#roiDataTransform *= QtGui.QTransform().translate(-rotBound.left(), -rotBound.top())
|
|
||||||
|
|
||||||
#else:
|
|
||||||
#arr2 = arr1
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
#### Shift off partial pixels
|
|
||||||
## 1. map ROI into current data space
|
|
||||||
#roiBounds = roiDataTransform.mapRect(self.boundingRect())
|
|
||||||
|
|
||||||
## 2. Determine amount to shift data
|
|
||||||
#shift = (int(roiBounds.left()) - roiBounds.left(), int(roiBounds.bottom()) - roiBounds.bottom())
|
|
||||||
#if abs(shift[0]) > 1e-6 or abs(shift[1]) > 1e-6:
|
|
||||||
## 3. pad array with 0s before shifting
|
|
||||||
#arr2a = np.zeros((arr2.shape[0]+2, arr2.shape[1]+2) + arr2.shape[2:], dtype=arr2.dtype)
|
|
||||||
#arr2a[1:-1, 1:-1] = arr2
|
|
||||||
|
|
||||||
## 4. shift array and udpate transforms
|
|
||||||
#arr3 = ndimage.shift(arr2a, shift + (0,)*(arr2.ndim-2), order=1)
|
|
||||||
#roiDataTransform *= QtGui.QTransform().translate(1+shift[0], 1+shift[1])
|
|
||||||
#else:
|
|
||||||
#arr3 = arr2
|
|
||||||
|
|
||||||
|
|
||||||
#### Extract needed region from rotated/shifted array
|
|
||||||
## 1. map ROI into current data space (round these values off--they should be exact integer values at this point)
|
|
||||||
#roiBounds = roiDataTransform.mapRect(self.boundingRect())
|
|
||||||
##print self, roiBounds.height()
|
|
||||||
##import traceback
|
|
||||||
##traceback.print_stack()
|
|
||||||
|
|
||||||
#roiBounds = QtCore.QRect(round(roiBounds.left()), round(roiBounds.top()), round(roiBounds.width()), round(roiBounds.height()))
|
|
||||||
|
|
||||||
##2. intersect ROI with data bounds
|
|
||||||
#dataBounds = roiBounds.intersect(QtCore.QRect(0, 0, arr3.shape[0], arr3.shape[1]))
|
|
||||||
|
|
||||||
##3. Extract data from array
|
|
||||||
#db = dataBounds
|
|
||||||
#bounds = (
|
|
||||||
#(db.left(), db.right()+1),
|
|
||||||
#(db.top(), db.bottom()+1)
|
|
||||||
#)
|
|
||||||
#arr4 = arr3[bounds[0][0]:bounds[0][1], bounds[1][0]:bounds[1][1]]
|
|
||||||
|
|
||||||
#### Create zero array in size of ROI
|
|
||||||
#arr5 = np.zeros((roiBounds.width(), roiBounds.height()) + arr4.shape[2:], dtype=arr4.dtype)
|
|
||||||
|
|
||||||
### Fill array with ROI data
|
|
||||||
#orig = Point(dataBounds.topLeft() - roiBounds.topLeft())
|
|
||||||
#subArr = arr5[orig[0]:orig[0]+arr4.shape[0], orig[1]:orig[1]+arr4.shape[1]]
|
|
||||||
#subArr[:] = arr4[:subArr.shape[0], :subArr.shape[1]]
|
|
||||||
|
|
||||||
|
|
||||||
### figure out the reverse transpose order
|
|
||||||
#tr2 = np.array(tr1)
|
|
||||||
#for i in range(0, len(tr2)):
|
|
||||||
#tr2[tr1[i]] = i
|
|
||||||
#tr2 = tuple(tr2)
|
|
||||||
|
|
||||||
### Untranspose array before returning
|
|
||||||
#return arr5.transpose(tr2)
|
|
||||||
|
|
||||||
def getAffineSliceParams(self, data, img, axes=(0,1)):
|
def getAffineSliceParams(self, data, img, axes=(0,1)):
|
||||||
"""
|
"""
|
||||||
@ -1088,7 +1170,18 @@ class ROI(GraphicsObject):
|
|||||||
|
|
||||||
|
|
||||||
class Handle(UIGraphicsItem):
|
class Handle(UIGraphicsItem):
|
||||||
|
"""
|
||||||
|
Handle represents a single user-interactable point attached to an ROI. They
|
||||||
|
are usually created by a call to one of the ROI.add___Handle() methods.
|
||||||
|
|
||||||
|
Handles are represented as a square, diamond, or circle, and are drawn with
|
||||||
|
fixed pixel size regardless of the scaling of the view they are displayed in.
|
||||||
|
|
||||||
|
Handles may be dragged to change the position, size, orientation, or other
|
||||||
|
properties of the ROI they are attached to.
|
||||||
|
|
||||||
|
|
||||||
|
"""
|
||||||
types = { ## defines number of sides, start angle for each handle type
|
types = { ## defines number of sides, start angle for each handle type
|
||||||
't': (4, np.pi/4),
|
't': (4, np.pi/4),
|
||||||
'f': (4, np.pi/4),
|
'f': (4, np.pi/4),
|
||||||
@ -1360,6 +1453,22 @@ class TestROI(ROI):
|
|||||||
|
|
||||||
|
|
||||||
class RectROI(ROI):
|
class RectROI(ROI):
|
||||||
|
"""
|
||||||
|
Rectangular ROI subclass with a single scale handle at the top-right corner.
|
||||||
|
|
||||||
|
============== =============================================================
|
||||||
|
**Arguments**
|
||||||
|
pos (length-2 sequence) The position of the ROI origin.
|
||||||
|
See ROI().
|
||||||
|
size (length-2 sequence) The size of the ROI. See ROI().
|
||||||
|
centered (bool) If True, scale handles affect the ROI relative to its
|
||||||
|
center, rather than its origin.
|
||||||
|
sideScalers (bool) If True, extra scale handles are added at the top and
|
||||||
|
right edges.
|
||||||
|
\**args All extra keyword arguments are passed to ROI()
|
||||||
|
============== =============================================================
|
||||||
|
|
||||||
|
"""
|
||||||
def __init__(self, pos, size, centered=False, sideScalers=False, **args):
|
def __init__(self, pos, size, centered=False, sideScalers=False, **args):
|
||||||
#QtGui.QGraphicsRectItem.__init__(self, 0, 0, size[0], size[1])
|
#QtGui.QGraphicsRectItem.__init__(self, 0, 0, size[0], size[1])
|
||||||
ROI.__init__(self, pos, size, **args)
|
ROI.__init__(self, pos, size, **args)
|
||||||
@ -1375,6 +1484,22 @@ class RectROI(ROI):
|
|||||||
self.addScaleHandle([0.5, 1], [0.5, center[1]])
|
self.addScaleHandle([0.5, 1], [0.5, center[1]])
|
||||||
|
|
||||||
class LineROI(ROI):
|
class LineROI(ROI):
|
||||||
|
"""
|
||||||
|
Rectangular ROI subclass with scale-rotate handles on either side. This
|
||||||
|
allows the ROI to be positioned as if moving the ends of a line segment.
|
||||||
|
A third handle controls the width of the ROI orthogonal to its "line" axis.
|
||||||
|
|
||||||
|
============== =============================================================
|
||||||
|
**Arguments**
|
||||||
|
pos1 (length-2 sequence) The position of the center of the ROI's
|
||||||
|
left edge.
|
||||||
|
pos2 (length-2 sequence) The position of the center of the ROI's
|
||||||
|
right edge.
|
||||||
|
width (float) The width of the ROI.
|
||||||
|
\**args All extra keyword arguments are passed to ROI()
|
||||||
|
============== =============================================================
|
||||||
|
|
||||||
|
"""
|
||||||
def __init__(self, pos1, pos2, width, **args):
|
def __init__(self, pos1, pos2, width, **args):
|
||||||
pos1 = Point(pos1)
|
pos1 = Point(pos1)
|
||||||
pos2 = Point(pos2)
|
pos2 = Point(pos2)
|
||||||
@ -1399,6 +1524,13 @@ class MultiRectROI(QtGui.QGraphicsObject):
|
|||||||
This is generally used to mark a curved path through
|
This is generally used to mark a curved path through
|
||||||
an image similarly to PolyLineROI. It differs in that each segment
|
an image similarly to PolyLineROI. It differs in that each segment
|
||||||
of the chain is rectangular instead of linear and thus has width.
|
of the chain is rectangular instead of linear and thus has width.
|
||||||
|
|
||||||
|
============== =============================================================
|
||||||
|
**Arguments**
|
||||||
|
points (list of length-2 sequences) The list of points in the path.
|
||||||
|
width (float) The width of the ROIs orthogonal to the path.
|
||||||
|
\**args All extra keyword arguments are passed to ROI()
|
||||||
|
============== =============================================================
|
||||||
"""
|
"""
|
||||||
sigRegionChangeFinished = QtCore.Signal(object)
|
sigRegionChangeFinished = QtCore.Signal(object)
|
||||||
sigRegionChangeStarted = QtCore.Signal(object)
|
sigRegionChangeStarted = QtCore.Signal(object)
|
||||||
@ -1523,6 +1655,18 @@ class MultiLineROI(MultiRectROI):
|
|||||||
print("Warning: MultiLineROI has been renamed to MultiRectROI. (and MultiLineROI may be redefined in the future)")
|
print("Warning: MultiLineROI has been renamed to MultiRectROI. (and MultiLineROI may be redefined in the future)")
|
||||||
|
|
||||||
class EllipseROI(ROI):
|
class EllipseROI(ROI):
|
||||||
|
"""
|
||||||
|
Elliptical ROI subclass with one scale handle and one rotation handle.
|
||||||
|
|
||||||
|
|
||||||
|
============== =============================================================
|
||||||
|
**Arguments**
|
||||||
|
pos (length-2 sequence) The position of the ROI's origin.
|
||||||
|
size (length-2 sequence) The size of the ROI's bounding rectangle.
|
||||||
|
\**args All extra keyword arguments are passed to ROI()
|
||||||
|
============== =============================================================
|
||||||
|
|
||||||
|
"""
|
||||||
def __init__(self, pos, size, **args):
|
def __init__(self, pos, size, **args):
|
||||||
#QtGui.QGraphicsRectItem.__init__(self, 0, 0, size[0], size[1])
|
#QtGui.QGraphicsRectItem.__init__(self, 0, 0, size[0], size[1])
|
||||||
ROI.__init__(self, pos, size, **args)
|
ROI.__init__(self, pos, size, **args)
|
||||||
@ -1540,6 +1684,10 @@ class EllipseROI(ROI):
|
|||||||
p.drawEllipse(r)
|
p.drawEllipse(r)
|
||||||
|
|
||||||
def getArrayRegion(self, arr, img=None):
|
def getArrayRegion(self, arr, img=None):
|
||||||
|
"""
|
||||||
|
Return the result of ROI.getArrayRegion() masked by the elliptical shape
|
||||||
|
of the ROI. Regions outside the ellipse are set to 0.
|
||||||
|
"""
|
||||||
arr = ROI.getArrayRegion(self, arr, img)
|
arr = ROI.getArrayRegion(self, arr, img)
|
||||||
if arr is None or arr.shape[0] == 0 or arr.shape[1] == 0:
|
if arr is None or arr.shape[0] == 0 or arr.shape[1] == 0:
|
||||||
return None
|
return None
|
||||||
@ -1557,12 +1705,25 @@ class EllipseROI(ROI):
|
|||||||
|
|
||||||
|
|
||||||
class CircleROI(EllipseROI):
|
class CircleROI(EllipseROI):
|
||||||
|
"""
|
||||||
|
Circular ROI subclass. Behaves exactly as EllipseROI, but may only be scaled
|
||||||
|
proportionally to maintain its aspect ratio.
|
||||||
|
|
||||||
|
============== =============================================================
|
||||||
|
**Arguments**
|
||||||
|
pos (length-2 sequence) The position of the ROI's origin.
|
||||||
|
size (length-2 sequence) The size of the ROI's bounding rectangle.
|
||||||
|
\**args All extra keyword arguments are passed to ROI()
|
||||||
|
============== =============================================================
|
||||||
|
|
||||||
|
"""
|
||||||
def __init__(self, pos, size, **args):
|
def __init__(self, pos, size, **args):
|
||||||
ROI.__init__(self, pos, size, **args)
|
ROI.__init__(self, pos, size, **args)
|
||||||
self.aspectLocked = True
|
self.aspectLocked = True
|
||||||
#self.addTranslateHandle([0.5, 0.5])
|
#self.addTranslateHandle([0.5, 0.5])
|
||||||
self.addScaleHandle([0.5*2.**-0.5 + 0.5, 0.5*2.**-0.5 + 0.5], [0.5, 0.5])
|
self.addScaleHandle([0.5*2.**-0.5 + 0.5, 0.5*2.**-0.5 + 0.5], [0.5, 0.5])
|
||||||
|
|
||||||
|
|
||||||
class PolygonROI(ROI):
|
class PolygonROI(ROI):
|
||||||
## deprecated. Use PloyLineROI instead.
|
## deprecated. Use PloyLineROI instead.
|
||||||
|
|
||||||
@ -1616,8 +1777,24 @@ class PolygonROI(ROI):
|
|||||||
return sc
|
return sc
|
||||||
|
|
||||||
class PolyLineROI(ROI):
|
class PolyLineROI(ROI):
|
||||||
"""Container class for multiple connected LineSegmentROIs. Responsible for adding new
|
"""
|
||||||
line segments, and for translation/(rotation?) of multiple lines together."""
|
Container class for multiple connected LineSegmentROIs.
|
||||||
|
|
||||||
|
This class allows the user to draw paths of multiple line segments.
|
||||||
|
|
||||||
|
============== =============================================================
|
||||||
|
**Arguments**
|
||||||
|
positions (list of length-2 sequences) The list of points in the path.
|
||||||
|
Note that, unlike the handle positions specified in other
|
||||||
|
ROIs, these positions must be expressed in the normal
|
||||||
|
coordinate system of the ROI, rather than (0 to 1) relative
|
||||||
|
to the size of the ROI.
|
||||||
|
closed (bool) if True, an extra LineSegmentROI is added connecting
|
||||||
|
the beginning and end points.
|
||||||
|
\**args All extra keyword arguments are passed to ROI()
|
||||||
|
============== =============================================================
|
||||||
|
|
||||||
|
"""
|
||||||
def __init__(self, positions, closed=False, pos=None, **args):
|
def __init__(self, positions, closed=False, pos=None, **args):
|
||||||
|
|
||||||
if pos is None:
|
if pos is None:
|
||||||
@ -1730,6 +1907,10 @@ class PolyLineROI(ROI):
|
|||||||
return p
|
return p
|
||||||
|
|
||||||
def getArrayRegion(self, data, img, axes=(0,1), returnMappedCoords=False, **kwds):
|
def getArrayRegion(self, data, img, axes=(0,1), returnMappedCoords=False, **kwds):
|
||||||
|
"""
|
||||||
|
Return the result of ROI.getArrayRegion(), masked by the shape of the
|
||||||
|
ROI. Values outside the ROI shape are set to 0.
|
||||||
|
"""
|
||||||
sl = self.getArraySlice(data, img, axes=(0,1))
|
sl = self.getArraySlice(data, img, axes=(0,1))
|
||||||
if sl is None:
|
if sl is None:
|
||||||
return None
|
return None
|
||||||
@ -1758,6 +1939,16 @@ class PolyLineROI(ROI):
|
|||||||
class LineSegmentROI(ROI):
|
class LineSegmentROI(ROI):
|
||||||
"""
|
"""
|
||||||
ROI subclass with two freely-moving handles defining a line.
|
ROI subclass with two freely-moving handles defining a line.
|
||||||
|
|
||||||
|
============== =============================================================
|
||||||
|
**Arguments**
|
||||||
|
positions (list of two length-2 sequences) The endpoints of the line
|
||||||
|
segment. Note that, unlike the handle positions specified in
|
||||||
|
other ROIs, these positions must be expressed in the normal
|
||||||
|
coordinate system of the ROI, rather than (0 to 1) relative
|
||||||
|
to the size of the ROI.
|
||||||
|
\**args All extra keyword arguments are passed to ROI()
|
||||||
|
============== =============================================================
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, positions=(None, None), pos=None, handles=(None,None), **args):
|
def __init__(self, positions=(None, None), pos=None, handles=(None,None), **args):
|
||||||
@ -1810,8 +2001,13 @@ class LineSegmentROI(ROI):
|
|||||||
|
|
||||||
def getArrayRegion(self, data, img, axes=(0,1)):
|
def getArrayRegion(self, data, img, axes=(0,1)):
|
||||||
"""
|
"""
|
||||||
Use the position of this ROI relative to an imageItem to pull a slice from an array.
|
Use the position of this ROI relative to an imageItem to pull a slice
|
||||||
Since this pulls 1D data from a 2D coordinate system, the return value will have ndim = data.ndim-1
|
from an array.
|
||||||
|
|
||||||
|
Since this pulls 1D data from a 2D coordinate system, the return value
|
||||||
|
will have ndim = data.ndim-1
|
||||||
|
|
||||||
|
See ROI.getArrayRegion() for a description of the arguments.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
imgPts = [self.mapToItem(img, h['item'].pos()) for h in self.handles]
|
imgPts = [self.mapToItem(img, h['item'].pos()) for h in self.handles]
|
||||||
|
@ -664,8 +664,14 @@ class ScatterPlotItem(GraphicsObject):
|
|||||||
if pxPad > 0:
|
if pxPad > 0:
|
||||||
# determine length of pixel in local x, y directions
|
# determine length of pixel in local x, y directions
|
||||||
px, py = self.pixelVectors()
|
px, py = self.pixelVectors()
|
||||||
px = 0 if px is None else px.length()
|
try:
|
||||||
py = 0 if py is None else py.length()
|
px = 0 if px is None else px.length()
|
||||||
|
except OverflowError:
|
||||||
|
px = 0
|
||||||
|
try:
|
||||||
|
py = 0 if py is None else py.length()
|
||||||
|
except OverflowError:
|
||||||
|
py = 0
|
||||||
|
|
||||||
# return bounds expanded by pixel size
|
# return bounds expanded by pixel size
|
||||||
px *= pxPad
|
px *= pxPad
|
||||||
|
@ -9,18 +9,18 @@ class TextItem(UIGraphicsItem):
|
|||||||
"""
|
"""
|
||||||
def __init__(self, text='', color=(200,200,200), html=None, anchor=(0,0), border=None, fill=None, angle=0):
|
def __init__(self, text='', color=(200,200,200), html=None, anchor=(0,0), border=None, fill=None, angle=0):
|
||||||
"""
|
"""
|
||||||
=========== =================================================================================
|
============== =================================================================================
|
||||||
Arguments:
|
**Arguments:**
|
||||||
*text* The text to display
|
*text* The text to display
|
||||||
*color* The color of the text (any format accepted by pg.mkColor)
|
*color* The color of the text (any format accepted by pg.mkColor)
|
||||||
*html* If specified, this overrides both *text* and *color*
|
*html* If specified, this overrides both *text* and *color*
|
||||||
*anchor* A QPointF or (x,y) sequence indicating what region of the text box will
|
*anchor* A QPointF or (x,y) sequence indicating what region of the text box will
|
||||||
be anchored to the item's position. A value of (0,0) sets the upper-left corner
|
be anchored to the item's position. A value of (0,0) sets the upper-left corner
|
||||||
of the text box to be at the position specified by setPos(), while a value of (1,1)
|
of the text box to be at the position specified by setPos(), while a value of (1,1)
|
||||||
sets the lower-right corner.
|
sets the lower-right corner.
|
||||||
*border* A pen to use when drawing the border
|
*border* A pen to use when drawing the border
|
||||||
*fill* A brush to use when filling within the border
|
*fill* A brush to use when filling within the border
|
||||||
=========== =================================================================================
|
============== =================================================================================
|
||||||
"""
|
"""
|
||||||
|
|
||||||
## not working yet
|
## not working yet
|
||||||
|
@ -19,15 +19,15 @@ class VTickGroup(UIGraphicsItem):
|
|||||||
"""
|
"""
|
||||||
def __init__(self, xvals=None, yrange=None, pen=None):
|
def __init__(self, xvals=None, yrange=None, pen=None):
|
||||||
"""
|
"""
|
||||||
============= ===================================================================
|
============== ===================================================================
|
||||||
**Arguments**
|
**Arguments:**
|
||||||
xvals A list of x values (in data coordinates) at which to draw ticks.
|
xvals A list of x values (in data coordinates) at which to draw ticks.
|
||||||
yrange A list of [low, high] limits for the tick. 0 is the bottom of
|
yrange A list of [low, high] limits for the tick. 0 is the bottom of
|
||||||
the view, 1 is the top. [0.8, 1] would draw ticks in the top
|
the view, 1 is the top. [0.8, 1] would draw ticks in the top
|
||||||
fifth of the view.
|
fifth of the view.
|
||||||
pen The pen to use for drawing ticks. Default is grey. Can be specified
|
pen The pen to use for drawing ticks. Default is grey. Can be specified
|
||||||
as any argument valid for :func:`mkPen<pyqtgraph.mkPen>`
|
as any argument valid for :func:`mkPen<pyqtgraph.mkPen>`
|
||||||
============= ===================================================================
|
============== ===================================================================
|
||||||
"""
|
"""
|
||||||
if yrange is None:
|
if yrange is None:
|
||||||
yrange = [0, 1]
|
yrange = [0, 1]
|
||||||
@ -56,10 +56,10 @@ class VTickGroup(UIGraphicsItem):
|
|||||||
def setXVals(self, vals):
|
def setXVals(self, vals):
|
||||||
"""Set the x values for the ticks.
|
"""Set the x values for the ticks.
|
||||||
|
|
||||||
============= =====================================================================
|
============== =====================================================================
|
||||||
**Arguments**
|
**Arguments:**
|
||||||
vals A list of x values (in data/plot coordinates) at which to draw ticks.
|
vals A list of x values (in data/plot coordinates) at which to draw ticks.
|
||||||
============= =====================================================================
|
============== =====================================================================
|
||||||
"""
|
"""
|
||||||
self.xvals = vals
|
self.xvals = vals
|
||||||
self.rebuildTicks()
|
self.rebuildTicks()
|
||||||
|
@ -5,28 +5,63 @@ from ...Point import Point
|
|||||||
from ... import functions as fn
|
from ... import functions as fn
|
||||||
from .. ItemGroup import ItemGroup
|
from .. ItemGroup import ItemGroup
|
||||||
from .. GraphicsWidget import GraphicsWidget
|
from .. GraphicsWidget import GraphicsWidget
|
||||||
from ...GraphicsScene import GraphicsScene
|
|
||||||
import weakref
|
import weakref
|
||||||
from copy import deepcopy
|
from copy import deepcopy
|
||||||
from ... import debug as debug
|
from ... import debug as debug
|
||||||
from ... import getConfigOption
|
from ... import getConfigOption
|
||||||
|
import sys
|
||||||
|
from pyqtgraph.Qt import isQObjectAlive
|
||||||
|
|
||||||
__all__ = ['ViewBox']
|
__all__ = ['ViewBox']
|
||||||
|
|
||||||
|
class WeakList(object):
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self._items = []
|
||||||
|
|
||||||
|
def append(self, obj):
|
||||||
|
#Add backwards to iterate backwards (to make iterating more efficient on removal).
|
||||||
|
self._items.insert(0, weakref.ref(obj))
|
||||||
|
|
||||||
|
def __iter__(self):
|
||||||
|
i = len(self._items)-1
|
||||||
|
while i >= 0:
|
||||||
|
ref = self._items[i]
|
||||||
|
d = ref()
|
||||||
|
if d is None:
|
||||||
|
del self._items[i]
|
||||||
|
else:
|
||||||
|
yield d
|
||||||
|
i -= 1
|
||||||
|
|
||||||
class ChildGroup(ItemGroup):
|
class ChildGroup(ItemGroup):
|
||||||
|
|
||||||
sigItemsChanged = QtCore.Signal()
|
|
||||||
def __init__(self, parent):
|
def __init__(self, parent):
|
||||||
ItemGroup.__init__(self, parent)
|
ItemGroup.__init__(self, parent)
|
||||||
|
|
||||||
|
# Used as callback to inform ViewBox when items are added/removed from
|
||||||
|
# the group.
|
||||||
|
# Note 1: We would prefer to override itemChange directly on the
|
||||||
|
# ViewBox, but this causes crashes on PySide.
|
||||||
|
# Note 2: We might also like to use a signal rather than this callback
|
||||||
|
# mechanism, but this causes a different PySide crash.
|
||||||
|
self.itemsChangedListeners = WeakList()
|
||||||
|
|
||||||
# excempt from telling view when transform changes
|
# excempt from telling view when transform changes
|
||||||
self._GraphicsObject__inform_view_on_change = False
|
self._GraphicsObject__inform_view_on_change = False
|
||||||
|
|
||||||
def itemChange(self, change, value):
|
def itemChange(self, change, value):
|
||||||
ret = ItemGroup.itemChange(self, change, value)
|
ret = ItemGroup.itemChange(self, change, value)
|
||||||
if change == self.ItemChildAddedChange or change == self.ItemChildRemovedChange:
|
if change == self.ItemChildAddedChange or change == self.ItemChildRemovedChange:
|
||||||
self.sigItemsChanged.emit()
|
try:
|
||||||
|
itemsChangedListeners = self.itemsChangedListeners
|
||||||
|
except AttributeError:
|
||||||
|
# It's possible that the attribute was already collected when the itemChange happened
|
||||||
|
# (if it was triggered during the gc of the object).
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
for listener in itemsChangedListeners:
|
||||||
|
listener.itemsChanged()
|
||||||
return ret
|
return ret
|
||||||
|
|
||||||
|
|
||||||
@ -71,16 +106,16 @@ class ViewBox(GraphicsWidget):
|
|||||||
|
|
||||||
def __init__(self, parent=None, border=None, lockAspect=False, enableMouse=True, invertY=False, enableMenu=True, name=None):
|
def __init__(self, parent=None, border=None, lockAspect=False, enableMouse=True, invertY=False, enableMenu=True, name=None):
|
||||||
"""
|
"""
|
||||||
============= =============================================================
|
============== =============================================================
|
||||||
**Arguments**
|
**Arguments:**
|
||||||
*parent* (QGraphicsWidget) Optional parent widget
|
*parent* (QGraphicsWidget) Optional parent widget
|
||||||
*border* (QPen) Do draw a border around the view, give any
|
*border* (QPen) Do draw a border around the view, give any
|
||||||
single argument accepted by :func:`mkPen <pyqtgraph.mkPen>`
|
single argument accepted by :func:`mkPen <pyqtgraph.mkPen>`
|
||||||
*lockAspect* (False or float) The aspect ratio to lock the view
|
*lockAspect* (False or float) The aspect ratio to lock the view
|
||||||
coorinates to. (or False to allow the ratio to change)
|
coorinates to. (or False to allow the ratio to change)
|
||||||
*enableMouse* (bool) Whether mouse can be used to scale/pan the view
|
*enableMouse* (bool) Whether mouse can be used to scale/pan the view
|
||||||
*invertY* (bool) See :func:`invertY <pyqtgraph.ViewBox.invertY>`
|
*invertY* (bool) See :func:`invertY <pyqtgraph.ViewBox.invertY>`
|
||||||
============= =============================================================
|
============== =============================================================
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
@ -118,6 +153,15 @@ class ViewBox(GraphicsWidget):
|
|||||||
'wheelScaleFactor': -1.0 / 8.0,
|
'wheelScaleFactor': -1.0 / 8.0,
|
||||||
|
|
||||||
'background': None,
|
'background': None,
|
||||||
|
|
||||||
|
# Limits
|
||||||
|
'limits': {
|
||||||
|
'xLimits': [None, None], # Maximum and minimum visible X values
|
||||||
|
'yLimits': [None, None], # Maximum and minimum visible Y values
|
||||||
|
'xRange': [None, None], # Maximum and minimum X range
|
||||||
|
'yRange': [None, None], # Maximum and minimum Y range
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
self._updatingRange = False ## Used to break recursive loops. See updateAutoRange.
|
self._updatingRange = False ## Used to break recursive loops. See updateAutoRange.
|
||||||
self._itemBoundsCache = weakref.WeakKeyDictionary()
|
self._itemBoundsCache = weakref.WeakKeyDictionary()
|
||||||
@ -131,7 +175,7 @@ class ViewBox(GraphicsWidget):
|
|||||||
## this is a workaround for a Qt + OpenGL bug that causes improper clipping
|
## this is a workaround for a Qt + OpenGL bug that causes improper clipping
|
||||||
## https://bugreports.qt.nokia.com/browse/QTBUG-23723
|
## https://bugreports.qt.nokia.com/browse/QTBUG-23723
|
||||||
self.childGroup = ChildGroup(self)
|
self.childGroup = ChildGroup(self)
|
||||||
self.childGroup.sigItemsChanged.connect(self.itemsChanged)
|
self.childGroup.itemsChangedListeners.append(self)
|
||||||
|
|
||||||
self.background = QtGui.QGraphicsRectItem(self.rect())
|
self.background = QtGui.QGraphicsRectItem(self.rect())
|
||||||
self.background.setParentItem(self)
|
self.background.setParentItem(self)
|
||||||
@ -197,6 +241,7 @@ class ViewBox(GraphicsWidget):
|
|||||||
del ViewBox.NamedViews[self.name]
|
del ViewBox.NamedViews[self.name]
|
||||||
|
|
||||||
def close(self):
|
def close(self):
|
||||||
|
self.clear()
|
||||||
self.unregister()
|
self.unregister()
|
||||||
|
|
||||||
def implements(self, interface):
|
def implements(self, interface):
|
||||||
@ -276,6 +321,17 @@ class ViewBox(GraphicsWidget):
|
|||||||
self.updateViewRange()
|
self.updateViewRange()
|
||||||
self.sigStateChanged.emit(self)
|
self.sigStateChanged.emit(self)
|
||||||
|
|
||||||
|
def setBackgroundColor(self, color):
|
||||||
|
"""
|
||||||
|
Set the background color of the ViewBox.
|
||||||
|
|
||||||
|
If color is None, then no background will be drawn.
|
||||||
|
|
||||||
|
Added in version 0.9.9
|
||||||
|
"""
|
||||||
|
self.background.setVisible(color is not None)
|
||||||
|
self.state['background'] = color
|
||||||
|
self.updateBackground()
|
||||||
|
|
||||||
def setMouseMode(self, mode):
|
def setMouseMode(self, mode):
|
||||||
"""
|
"""
|
||||||
@ -398,13 +454,20 @@ class ViewBox(GraphicsWidget):
|
|||||||
print("make qrectf failed:", self.state['targetRange'])
|
print("make qrectf failed:", self.state['targetRange'])
|
||||||
raise
|
raise
|
||||||
|
|
||||||
|
def _resetTarget(self):
|
||||||
|
# Reset target range to exactly match current view range.
|
||||||
|
# This is used during mouse interaction to prevent unpredictable
|
||||||
|
# behavior (because the user is unaware of targetRange).
|
||||||
|
if self.state['aspectLocked'] is False: # (interferes with aspect locking)
|
||||||
|
self.state['targetRange'] = [self.state['viewRange'][0][:], self.state['viewRange'][1][:]]
|
||||||
|
|
||||||
def setRange(self, rect=None, xRange=None, yRange=None, padding=None, update=True, disableAutoRange=True):
|
def setRange(self, rect=None, xRange=None, yRange=None, padding=None, update=True, disableAutoRange=True):
|
||||||
"""
|
"""
|
||||||
Set the visible range of the ViewBox.
|
Set the visible range of the ViewBox.
|
||||||
Must specify at least one of *rect*, *xRange*, or *yRange*.
|
Must specify at least one of *rect*, *xRange*, or *yRange*.
|
||||||
|
|
||||||
================== =====================================================================
|
================== =====================================================================
|
||||||
**Arguments**
|
**Arguments:**
|
||||||
*rect* (QRectF) The full range that should be visible in the view box.
|
*rect* (QRectF) The full range that should be visible in the view box.
|
||||||
*xRange* (min,max) The range that should be visible along the x-axis.
|
*xRange* (min,max) The range that should be visible along the x-axis.
|
||||||
*yRange* (min,max) The range that should be visible along the y-axis.
|
*yRange* (min,max) The range that should be visible along the y-axis.
|
||||||
@ -546,14 +609,14 @@ class ViewBox(GraphicsWidget):
|
|||||||
Note that this is not the same as enableAutoRange, which causes the view to
|
Note that this is not the same as enableAutoRange, which causes the view to
|
||||||
automatically auto-range whenever its contents are changed.
|
automatically auto-range whenever its contents are changed.
|
||||||
|
|
||||||
=========== ============================================================
|
============== ============================================================
|
||||||
Arguments
|
**Arguments:**
|
||||||
padding The fraction of the total data range to add on to the final
|
padding The fraction of the total data range to add on to the final
|
||||||
visible range. By default, this value is set between 0.02
|
visible range. By default, this value is set between 0.02
|
||||||
and 0.1 depending on the size of the ViewBox.
|
and 0.1 depending on the size of the ViewBox.
|
||||||
items If specified, this is a list of items to consider when
|
items If specified, this is a list of items to consider when
|
||||||
determining the visible range.
|
determining the visible range.
|
||||||
=========== ============================================================
|
============== ============================================================
|
||||||
"""
|
"""
|
||||||
if item is None:
|
if item is None:
|
||||||
bounds = self.childrenBoundingRect(items=items)
|
bounds = self.childrenBoundingRect(items=items)
|
||||||
@ -571,6 +634,57 @@ class ViewBox(GraphicsWidget):
|
|||||||
else:
|
else:
|
||||||
padding = 0.02
|
padding = 0.02
|
||||||
return padding
|
return padding
|
||||||
|
|
||||||
|
def setLimits(self, **kwds):
|
||||||
|
"""
|
||||||
|
Set limits that constrain the possible view ranges.
|
||||||
|
|
||||||
|
**Panning limits**. The following arguments define the region within the
|
||||||
|
viewbox coordinate system that may be accessed by panning the view.
|
||||||
|
|
||||||
|
=========== ============================================================
|
||||||
|
xMin Minimum allowed x-axis value
|
||||||
|
xMax Maximum allowed x-axis value
|
||||||
|
yMin Minimum allowed y-axis value
|
||||||
|
yMax Maximum allowed y-axis value
|
||||||
|
=========== ============================================================
|
||||||
|
|
||||||
|
**Scaling limits**. These arguments prevent the view being zoomed in or
|
||||||
|
out too far.
|
||||||
|
|
||||||
|
=========== ============================================================
|
||||||
|
minXRange Minimum allowed left-to-right span across the view.
|
||||||
|
maxXRange Maximum allowed left-to-right span across the view.
|
||||||
|
minYRange Minimum allowed top-to-bottom span across the view.
|
||||||
|
maxYRange Maximum allowed top-to-bottom span across the view.
|
||||||
|
=========== ============================================================
|
||||||
|
|
||||||
|
Added in version 0.9.9
|
||||||
|
"""
|
||||||
|
update = False
|
||||||
|
|
||||||
|
#for kwd in ['xLimits', 'yLimits', 'minRange', 'maxRange']:
|
||||||
|
#if kwd in kwds and self.state['limits'][kwd] != kwds[kwd]:
|
||||||
|
#self.state['limits'][kwd] = kwds[kwd]
|
||||||
|
#update = True
|
||||||
|
for axis in [0,1]:
|
||||||
|
for mnmx in [0,1]:
|
||||||
|
kwd = [['xMin', 'xMax'], ['yMin', 'yMax']][axis][mnmx]
|
||||||
|
lname = ['xLimits', 'yLimits'][axis]
|
||||||
|
if kwd in kwds and self.state['limits'][lname][mnmx] != kwds[kwd]:
|
||||||
|
self.state['limits'][lname][mnmx] = kwds[kwd]
|
||||||
|
update = True
|
||||||
|
kwd = [['minXRange', 'maxXRange'], ['minYRange', 'maxYRange']][axis][mnmx]
|
||||||
|
lname = ['xRange', 'yRange'][axis]
|
||||||
|
if kwd in kwds and self.state['limits'][lname][mnmx] != kwds[kwd]:
|
||||||
|
self.state['limits'][lname][mnmx] = kwds[kwd]
|
||||||
|
update = True
|
||||||
|
|
||||||
|
if update:
|
||||||
|
self.updateViewRange()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def scaleBy(self, s=None, center=None, x=None, y=None):
|
def scaleBy(self, s=None, center=None, x=None, y=None):
|
||||||
"""
|
"""
|
||||||
@ -818,7 +932,7 @@ class ViewBox(GraphicsWidget):
|
|||||||
try:
|
try:
|
||||||
getattr(oldLink, signal).disconnect(slot)
|
getattr(oldLink, signal).disconnect(slot)
|
||||||
oldLink.sigResized.disconnect(slot)
|
oldLink.sigResized.disconnect(slot)
|
||||||
except TypeError:
|
except (TypeError, RuntimeError):
|
||||||
## This can occur if the view has been deleted already
|
## This can occur if the view has been deleted already
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@ -1056,6 +1170,7 @@ class ViewBox(GraphicsWidget):
|
|||||||
center = Point(fn.invertQTransform(self.childGroup.transform()).map(ev.pos()))
|
center = Point(fn.invertQTransform(self.childGroup.transform()).map(ev.pos()))
|
||||||
#center = ev.pos()
|
#center = ev.pos()
|
||||||
|
|
||||||
|
self._resetTarget()
|
||||||
self.scaleBy(s, center)
|
self.scaleBy(s, center)
|
||||||
self.sigRangeChangedManually.emit(self.state['mouseEnabled'])
|
self.sigRangeChangedManually.emit(self.state['mouseEnabled'])
|
||||||
ev.accept()
|
ev.accept()
|
||||||
@ -1113,7 +1228,9 @@ class ViewBox(GraphicsWidget):
|
|||||||
x = tr.x() if mask[0] == 1 else None
|
x = tr.x() if mask[0] == 1 else None
|
||||||
y = tr.y() if mask[1] == 1 else None
|
y = tr.y() if mask[1] == 1 else None
|
||||||
|
|
||||||
self.translateBy(x=x, y=y)
|
self._resetTarget()
|
||||||
|
if x is not None or y is not None:
|
||||||
|
self.translateBy(x=x, y=y)
|
||||||
self.sigRangeChangedManually.emit(self.state['mouseEnabled'])
|
self.sigRangeChangedManually.emit(self.state['mouseEnabled'])
|
||||||
elif ev.button() & QtCore.Qt.RightButton:
|
elif ev.button() & QtCore.Qt.RightButton:
|
||||||
#print "vb.rightDrag"
|
#print "vb.rightDrag"
|
||||||
@ -1132,6 +1249,7 @@ class ViewBox(GraphicsWidget):
|
|||||||
y = s[1] if mouseEnabled[1] == 1 else None
|
y = s[1] if mouseEnabled[1] == 1 else None
|
||||||
|
|
||||||
center = Point(tr.map(ev.buttonDownPos(QtCore.Qt.RightButton)))
|
center = Point(tr.map(ev.buttonDownPos(QtCore.Qt.RightButton)))
|
||||||
|
self._resetTarget()
|
||||||
self.scaleBy(x=x, y=y, center=center)
|
self.scaleBy(x=x, y=y, center=center)
|
||||||
self.sigRangeChangedManually.emit(self.state['mouseEnabled'])
|
self.sigRangeChangedManually.emit(self.state['mouseEnabled'])
|
||||||
|
|
||||||
@ -1327,9 +1445,9 @@ class ViewBox(GraphicsWidget):
|
|||||||
viewRange = [self.state['targetRange'][0][:], self.state['targetRange'][1][:]]
|
viewRange = [self.state['targetRange'][0][:], self.state['targetRange'][1][:]]
|
||||||
changed = [False, False]
|
changed = [False, False]
|
||||||
|
|
||||||
# Make correction for aspect ratio constraint
|
#-------- Make correction for aspect ratio constraint ----------
|
||||||
|
|
||||||
## aspect is (widget w/h) / (view range w/h)
|
# aspect is (widget w/h) / (view range w/h)
|
||||||
aspect = self.state['aspectLocked'] # size ratio / view ratio
|
aspect = self.state['aspectLocked'] # size ratio / view ratio
|
||||||
tr = self.targetRect()
|
tr = self.targetRect()
|
||||||
bounds = self.rect()
|
bounds = self.rect()
|
||||||
@ -1351,7 +1469,6 @@ class ViewBox(GraphicsWidget):
|
|||||||
# then make the entire target range visible
|
# then make the entire target range visible
|
||||||
ax = 0 if targetRatio > viewRatio else 1
|
ax = 0 if targetRatio > viewRatio else 1
|
||||||
|
|
||||||
#### these should affect viewRange, not targetRange!
|
|
||||||
if ax == 0:
|
if ax == 0:
|
||||||
## view range needs to be taller than target
|
## view range needs to be taller than target
|
||||||
dy = 0.5 * (tr.width() / viewRatio - tr.height())
|
dy = 0.5 * (tr.width() / viewRatio - tr.height())
|
||||||
@ -1364,8 +1481,59 @@ class ViewBox(GraphicsWidget):
|
|||||||
if dx != 0:
|
if dx != 0:
|
||||||
changed[0] = True
|
changed[0] = True
|
||||||
viewRange[0] = [self.state['targetRange'][0][0] - dx, self.state['targetRange'][0][1] + dx]
|
viewRange[0] = [self.state['targetRange'][0][0] - dx, self.state['targetRange'][0][1] + dx]
|
||||||
|
|
||||||
|
# ----------- Make corrections for view limits -----------
|
||||||
|
|
||||||
|
limits = (self.state['limits']['xLimits'], self.state['limits']['yLimits'])
|
||||||
|
minRng = [self.state['limits']['xRange'][0], self.state['limits']['yRange'][0]]
|
||||||
|
maxRng = [self.state['limits']['xRange'][1], self.state['limits']['yRange'][1]]
|
||||||
|
|
||||||
|
for axis in [0, 1]:
|
||||||
|
if limits[axis][0] is None and limits[axis][1] is None and minRng[axis] is None and maxRng[axis] is None:
|
||||||
|
continue
|
||||||
|
|
||||||
changed = [(viewRange[i][0] != self.state['viewRange'][i][0]) and (viewRange[i][1] != self.state['viewRange'][i][1]) for i in (0,1)]
|
# max range cannot be larger than bounds, if they are given
|
||||||
|
if limits[axis][0] is not None and limits[axis][1] is not None:
|
||||||
|
if maxRng[axis] is not None:
|
||||||
|
maxRng[axis] = min(maxRng[axis], limits[axis][1]-limits[axis][0])
|
||||||
|
else:
|
||||||
|
maxRng[axis] = limits[axis][1]-limits[axis][0]
|
||||||
|
|
||||||
|
#print "\nLimits for axis %d: range=%s min=%s max=%s" % (axis, limits[axis], minRng[axis], maxRng[axis])
|
||||||
|
#print "Starting range:", viewRange[axis]
|
||||||
|
|
||||||
|
# Apply xRange, yRange
|
||||||
|
diff = viewRange[axis][1] - viewRange[axis][0]
|
||||||
|
if maxRng[axis] is not None and diff > maxRng[axis]:
|
||||||
|
delta = maxRng[axis] - diff
|
||||||
|
changed[axis] = True
|
||||||
|
elif minRng[axis] is not None and diff < minRng[axis]:
|
||||||
|
delta = minRng[axis] - diff
|
||||||
|
changed[axis] = True
|
||||||
|
else:
|
||||||
|
delta = 0
|
||||||
|
|
||||||
|
viewRange[axis][0] -= delta/2.
|
||||||
|
viewRange[axis][1] += delta/2.
|
||||||
|
|
||||||
|
#print "after applying min/max:", viewRange[axis]
|
||||||
|
|
||||||
|
# Apply xLimits, yLimits
|
||||||
|
mn, mx = limits[axis]
|
||||||
|
if mn is not None and viewRange[axis][0] < mn:
|
||||||
|
delta = mn - viewRange[axis][0]
|
||||||
|
viewRange[axis][0] += delta
|
||||||
|
viewRange[axis][1] += delta
|
||||||
|
changed[axis] = True
|
||||||
|
elif mx is not None and viewRange[axis][1] > mx:
|
||||||
|
delta = mx - viewRange[axis][1]
|
||||||
|
viewRange[axis][0] += delta
|
||||||
|
viewRange[axis][1] += delta
|
||||||
|
changed[axis] = True
|
||||||
|
|
||||||
|
#print "after applying edge limits:", viewRange[axis]
|
||||||
|
|
||||||
|
changed = [(viewRange[i][0] != self.state['viewRange'][i][0]) or (viewRange[i][1] != self.state['viewRange'][i][1]) for i in (0,1)]
|
||||||
self.state['viewRange'] = viewRange
|
self.state['viewRange'] = viewRange
|
||||||
|
|
||||||
# emit range change signals
|
# emit range change signals
|
||||||
@ -1493,6 +1661,9 @@ class ViewBox(GraphicsWidget):
|
|||||||
## called when the application is about to exit.
|
## called when the application is about to exit.
|
||||||
## this disables all callbacks, which might otherwise generate errors if invoked during exit.
|
## this disables all callbacks, which might otherwise generate errors if invoked during exit.
|
||||||
for k in ViewBox.AllViews:
|
for k in ViewBox.AllViews:
|
||||||
|
if isQObjectAlive(k) and getConfigOption('crashWarning'):
|
||||||
|
sys.stderr.write('Warning: ViewBox should be closed before application exit.\n')
|
||||||
|
|
||||||
try:
|
try:
|
||||||
k.destroyed.disconnect()
|
k.destroyed.disconnect()
|
||||||
except RuntimeError: ## signal is already disconnected.
|
except RuntimeError: ## signal is already disconnected.
|
||||||
|
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):
|
class GraphicsWindow(GraphicsLayoutWidget):
|
||||||
|
"""
|
||||||
|
Convenience subclass of :class:`GraphicsLayoutWidget
|
||||||
|
<pyqtgraph.GraphicsLayoutWidget>`. This class is intended for use from
|
||||||
|
the interactive python prompt.
|
||||||
|
"""
|
||||||
def __init__(self, title=None, size=(800,600), **kargs):
|
def __init__(self, title=None, size=(800,600), **kargs):
|
||||||
mkQApp()
|
mkQApp()
|
||||||
#self.win = QtGui.QMainWindow()
|
|
||||||
GraphicsLayoutWidget.__init__(self, **kargs)
|
GraphicsLayoutWidget.__init__(self, **kargs)
|
||||||
#self.win.setCentralWidget(self)
|
|
||||||
self.resize(*size)
|
self.resize(*size)
|
||||||
if title is not None:
|
if title is not None:
|
||||||
self.setWindowTitle(title)
|
self.setWindowTitle(title)
|
||||||
|
@ -33,6 +33,11 @@ from .. import debug as debug
|
|||||||
|
|
||||||
from ..SignalProxy import SignalProxy
|
from ..SignalProxy import SignalProxy
|
||||||
|
|
||||||
|
try:
|
||||||
|
from bottleneck import nanmin, nanmax
|
||||||
|
except ImportError:
|
||||||
|
from numpy import nanmin, nanmax
|
||||||
|
|
||||||
#try:
|
#try:
|
||||||
#from .. import metaarray as metaarray
|
#from .. import metaarray as metaarray
|
||||||
#HAVE_METAARRAY = True
|
#HAVE_METAARRAY = True
|
||||||
@ -196,7 +201,12 @@ class ImageView(QtGui.QWidget):
|
|||||||
img = img.asarray()
|
img = img.asarray()
|
||||||
|
|
||||||
if not isinstance(img, np.ndarray):
|
if not isinstance(img, np.ndarray):
|
||||||
raise Exception("Image must be specified as ndarray.")
|
required = ['dtype', 'max', 'min', 'ndim', 'shape', 'size']
|
||||||
|
if not all([hasattr(img, attr) for attr in required]):
|
||||||
|
raise TypeError("Image must be NumPy array or any object "
|
||||||
|
"that provides compatible attributes/methods:\n"
|
||||||
|
" %s" % str(required))
|
||||||
|
|
||||||
self.image = img
|
self.image = img
|
||||||
self.imageDisp = None
|
self.imageDisp = None
|
||||||
|
|
||||||
@ -319,11 +329,10 @@ class ImageView(QtGui.QWidget):
|
|||||||
if self.imageDisp is None:
|
if self.imageDisp is None:
|
||||||
image = self.normalize(self.image)
|
image = self.normalize(self.image)
|
||||||
self.imageDisp = image
|
self.imageDisp = image
|
||||||
self.levelMin, self.levelMax = list(map(float, ImageView.quickMinMax(self.imageDisp)))
|
self.levelMin, self.levelMax = list(map(float, self.quickMinMax(self.imageDisp)))
|
||||||
|
|
||||||
return self.imageDisp
|
return self.imageDisp
|
||||||
|
|
||||||
|
|
||||||
def close(self):
|
def close(self):
|
||||||
"""Closes the widget nicely, making sure to clear the graphics scene and release memory."""
|
"""Closes the widget nicely, making sure to clear the graphics scene and release memory."""
|
||||||
self.ui.roiPlot.close()
|
self.ui.roiPlot.close()
|
||||||
@ -375,7 +384,6 @@ class ImageView(QtGui.QWidget):
|
|||||||
else:
|
else:
|
||||||
QtGui.QWidget.keyReleaseEvent(self, ev)
|
QtGui.QWidget.keyReleaseEvent(self, ev)
|
||||||
|
|
||||||
|
|
||||||
def evalKeyState(self):
|
def evalKeyState(self):
|
||||||
if len(self.keysPressed) == 1:
|
if len(self.keysPressed) == 1:
|
||||||
key = list(self.keysPressed.keys())[0]
|
key = list(self.keysPressed.keys())[0]
|
||||||
@ -399,16 +407,13 @@ class ImageView(QtGui.QWidget):
|
|||||||
else:
|
else:
|
||||||
self.play(0)
|
self.play(0)
|
||||||
|
|
||||||
|
|
||||||
def timeout(self):
|
def timeout(self):
|
||||||
now = ptime.time()
|
now = ptime.time()
|
||||||
dt = now - self.lastPlayTime
|
dt = now - self.lastPlayTime
|
||||||
if dt < 0:
|
if dt < 0:
|
||||||
return
|
return
|
||||||
n = int(self.playRate * dt)
|
n = int(self.playRate * dt)
|
||||||
#print n, dt
|
|
||||||
if n != 0:
|
if n != 0:
|
||||||
#print n, dt, self.lastPlayTime
|
|
||||||
self.lastPlayTime += (float(n)/self.playRate)
|
self.lastPlayTime += (float(n)/self.playRate)
|
||||||
if self.currentIndex+n > self.image.shape[0]:
|
if self.currentIndex+n > self.image.shape[0]:
|
||||||
self.play(0)
|
self.play(0)
|
||||||
@ -433,17 +438,14 @@ class ImageView(QtGui.QWidget):
|
|||||||
self.autoLevels()
|
self.autoLevels()
|
||||||
self.roiChanged()
|
self.roiChanged()
|
||||||
self.sigProcessingChanged.emit(self)
|
self.sigProcessingChanged.emit(self)
|
||||||
|
|
||||||
|
|
||||||
def updateNorm(self):
|
def updateNorm(self):
|
||||||
if self.ui.normTimeRangeCheck.isChecked():
|
if self.ui.normTimeRangeCheck.isChecked():
|
||||||
#print "show!"
|
|
||||||
self.normRgn.show()
|
self.normRgn.show()
|
||||||
else:
|
else:
|
||||||
self.normRgn.hide()
|
self.normRgn.hide()
|
||||||
|
|
||||||
if self.ui.normROICheck.isChecked():
|
if self.ui.normROICheck.isChecked():
|
||||||
#print "show!"
|
|
||||||
self.normRoi.show()
|
self.normRoi.show()
|
||||||
else:
|
else:
|
||||||
self.normRoi.hide()
|
self.normRoi.hide()
|
||||||
@ -519,21 +521,25 @@ class ImageView(QtGui.QWidget):
|
|||||||
coords = coords - coords[:,0,np.newaxis]
|
coords = coords - coords[:,0,np.newaxis]
|
||||||
xvals = (coords**2).sum(axis=0) ** 0.5
|
xvals = (coords**2).sum(axis=0) ** 0.5
|
||||||
self.roiCurve.setData(y=data, x=xvals)
|
self.roiCurve.setData(y=data, x=xvals)
|
||||||
|
|
||||||
#self.ui.roiPlot.replot()
|
|
||||||
|
|
||||||
|
def quickMinMax(self, data):
|
||||||
@staticmethod
|
"""
|
||||||
def quickMinMax(data):
|
Estimate the min/max values of *data* by subsampling.
|
||||||
|
"""
|
||||||
while data.size > 1e6:
|
while data.size > 1e6:
|
||||||
ax = np.argmax(data.shape)
|
ax = np.argmax(data.shape)
|
||||||
sl = [slice(None)] * data.ndim
|
sl = [slice(None)] * data.ndim
|
||||||
sl[ax] = slice(None, None, 2)
|
sl[ax] = slice(None, None, 2)
|
||||||
data = data[sl]
|
data = data[sl]
|
||||||
return data.min(), data.max()
|
return nanmin(data), nanmax(data)
|
||||||
|
|
||||||
def normalize(self, image):
|
def normalize(self, image):
|
||||||
|
"""
|
||||||
|
Process *image* using the normalization options configured in the
|
||||||
|
control panel.
|
||||||
|
|
||||||
|
This can be repurposed to process any data through the same filter.
|
||||||
|
"""
|
||||||
if self.ui.normOffRadio.isChecked():
|
if self.ui.normOffRadio.isChecked():
|
||||||
return image
|
return image
|
||||||
|
|
||||||
|
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):
|
def __init__(self, tasks=None, workers=None, block=True, progressDialog=None, randomReseed=True, **kwds):
|
||||||
"""
|
"""
|
||||||
=============== ===================================================================
|
=============== ===================================================================
|
||||||
Arguments:
|
**Arguments:**
|
||||||
tasks list of objects to be processed (Parallelize will determine how to
|
tasks list of objects to be processed (Parallelize will determine how to
|
||||||
distribute the tasks). If unspecified, then each worker will receive
|
distribute the tasks). If unspecified, then each worker will receive
|
||||||
a single task with a unique id number.
|
a single task with a unique id number.
|
||||||
|
@ -1,13 +1,15 @@
|
|||||||
from .remoteproxy import RemoteEventHandler, ClosedError, NoResultError, LocalObjectProxy, ObjectProxy
|
|
||||||
import subprocess, atexit, os, sys, time, random, socket, signal
|
import subprocess, atexit, os, sys, time, random, socket, signal
|
||||||
import multiprocessing.connection
|
import multiprocessing.connection
|
||||||
from ..Qt import USE_PYSIDE
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
import cPickle as pickle
|
import cPickle as pickle
|
||||||
except ImportError:
|
except ImportError:
|
||||||
import pickle
|
import pickle
|
||||||
|
|
||||||
|
from .remoteproxy import RemoteEventHandler, ClosedError, NoResultError, LocalObjectProxy, ObjectProxy
|
||||||
|
from ..Qt import USE_PYSIDE
|
||||||
|
from ..util import cprint # color printing for debugging
|
||||||
|
|
||||||
|
|
||||||
__all__ = ['Process', 'QtProcess', 'ForkedProcess', 'ClosedError', 'NoResultError']
|
__all__ = ['Process', 'QtProcess', 'ForkedProcess', 'ClosedError', 'NoResultError']
|
||||||
|
|
||||||
class Process(RemoteEventHandler):
|
class Process(RemoteEventHandler):
|
||||||
@ -35,28 +37,29 @@ class Process(RemoteEventHandler):
|
|||||||
return objects either by proxy or by value (if they are picklable). See
|
return objects either by proxy or by value (if they are picklable). See
|
||||||
ProxyObject for more information.
|
ProxyObject for more information.
|
||||||
"""
|
"""
|
||||||
|
_process_count = 1 # just used for assigning colors to each process for debugging
|
||||||
|
|
||||||
def __init__(self, name=None, target=None, executable=None, copySysPath=True, debug=False, timeout=20, wrapStdout=None):
|
def __init__(self, name=None, target=None, executable=None, copySysPath=True, debug=False, timeout=20, wrapStdout=None):
|
||||||
"""
|
"""
|
||||||
============ =============================================================
|
============== =============================================================
|
||||||
Arguments:
|
**Arguments:**
|
||||||
name Optional name for this process used when printing messages
|
name Optional name for this process used when printing messages
|
||||||
from the remote process.
|
from the remote process.
|
||||||
target Optional function to call after starting remote process.
|
target Optional function to call after starting remote process.
|
||||||
By default, this is startEventLoop(), which causes the remote
|
By default, this is startEventLoop(), which causes the remote
|
||||||
process to process requests from the parent process until it
|
process to process requests from the parent process until it
|
||||||
is asked to quit. If you wish to specify a different target,
|
is asked to quit. If you wish to specify a different target,
|
||||||
it must be picklable (bound methods are not).
|
it must be picklable (bound methods are not).
|
||||||
copySysPath If True, copy the contents of sys.path to the remote process
|
copySysPath If True, copy the contents of sys.path to the remote process
|
||||||
debug If True, print detailed information about communication
|
debug If True, print detailed information about communication
|
||||||
with the child process.
|
with the child process.
|
||||||
wrapStdout If True (default on windows) then stdout and stderr from the
|
wrapStdout If True (default on windows) then stdout and stderr from the
|
||||||
child process will be caught by the parent process and
|
child process will be caught by the parent process and
|
||||||
forwarded to its stdout/stderr. This provides a workaround
|
forwarded to its stdout/stderr. This provides a workaround
|
||||||
for a python bug: http://bugs.python.org/issue3905
|
for a python bug: http://bugs.python.org/issue3905
|
||||||
but has the side effect that child output is significantly
|
but has the side effect that child output is significantly
|
||||||
delayed relative to the parent output.
|
delayed relative to the parent output.
|
||||||
============ =============================================================
|
============== =============================================================
|
||||||
"""
|
"""
|
||||||
if target is None:
|
if target is None:
|
||||||
target = startEventLoop
|
target = startEventLoop
|
||||||
@ -64,7 +67,7 @@ class Process(RemoteEventHandler):
|
|||||||
name = str(self)
|
name = str(self)
|
||||||
if executable is None:
|
if executable is None:
|
||||||
executable = sys.executable
|
executable = sys.executable
|
||||||
self.debug = debug
|
self.debug = 7 if debug is True else False # 7 causes printing in white
|
||||||
|
|
||||||
## random authentication key
|
## random authentication key
|
||||||
authkey = os.urandom(20)
|
authkey = os.urandom(20)
|
||||||
@ -75,21 +78,20 @@ class Process(RemoteEventHandler):
|
|||||||
|
|
||||||
#print "key:", ' '.join([str(ord(x)) for x in authkey])
|
#print "key:", ' '.join([str(ord(x)) for x in authkey])
|
||||||
## Listen for connection from remote process (and find free port number)
|
## Listen for connection from remote process (and find free port number)
|
||||||
port = 10000
|
l = multiprocessing.connection.Listener(('localhost', 0), authkey=authkey)
|
||||||
while True:
|
port = l.address[1]
|
||||||
try:
|
|
||||||
l = multiprocessing.connection.Listener(('localhost', int(port)), authkey=authkey)
|
|
||||||
break
|
|
||||||
except socket.error as ex:
|
|
||||||
if ex.errno != 98 and ex.errno != 10048: # unix=98, win=10048
|
|
||||||
raise
|
|
||||||
port += 1
|
|
||||||
|
|
||||||
|
|
||||||
## start remote process, instruct it to run target function
|
## start remote process, instruct it to run target function
|
||||||
sysPath = sys.path if copySysPath else None
|
sysPath = sys.path if copySysPath else None
|
||||||
bootstrap = os.path.abspath(os.path.join(os.path.dirname(__file__), 'bootstrap.py'))
|
bootstrap = os.path.abspath(os.path.join(os.path.dirname(__file__), 'bootstrap.py'))
|
||||||
self.debugMsg('Starting child process (%s %s)' % (executable, bootstrap))
|
self.debugMsg('Starting child process (%s %s)' % (executable, bootstrap))
|
||||||
|
|
||||||
|
# Decide on printing color for this process
|
||||||
|
if debug:
|
||||||
|
procDebug = (Process._process_count%6) + 1 # pick a color for this process to print in
|
||||||
|
Process._process_count += 1
|
||||||
|
else:
|
||||||
|
procDebug = False
|
||||||
|
|
||||||
if wrapStdout is None:
|
if wrapStdout is None:
|
||||||
wrapStdout = sys.platform.startswith('win')
|
wrapStdout = sys.platform.startswith('win')
|
||||||
@ -102,8 +104,8 @@ class Process(RemoteEventHandler):
|
|||||||
self.proc = subprocess.Popen((executable, bootstrap), stdin=subprocess.PIPE, stdout=stdout, stderr=stderr)
|
self.proc = subprocess.Popen((executable, bootstrap), stdin=subprocess.PIPE, stdout=stdout, stderr=stderr)
|
||||||
## to circumvent the bug and still make the output visible, we use
|
## to circumvent the bug and still make the output visible, we use
|
||||||
## background threads to pass data from pipes to stdout/stderr
|
## background threads to pass data from pipes to stdout/stderr
|
||||||
self._stdoutForwarder = FileForwarder(self.proc.stdout, "stdout")
|
self._stdoutForwarder = FileForwarder(self.proc.stdout, "stdout", procDebug)
|
||||||
self._stderrForwarder = FileForwarder(self.proc.stderr, "stderr")
|
self._stderrForwarder = FileForwarder(self.proc.stderr, "stderr", procDebug)
|
||||||
else:
|
else:
|
||||||
self.proc = subprocess.Popen((executable, bootstrap), stdin=subprocess.PIPE)
|
self.proc = subprocess.Popen((executable, bootstrap), stdin=subprocess.PIPE)
|
||||||
|
|
||||||
@ -120,7 +122,7 @@ class Process(RemoteEventHandler):
|
|||||||
targetStr=targetStr,
|
targetStr=targetStr,
|
||||||
path=sysPath,
|
path=sysPath,
|
||||||
pyside=USE_PYSIDE,
|
pyside=USE_PYSIDE,
|
||||||
debug=debug
|
debug=procDebug
|
||||||
)
|
)
|
||||||
pickle.dump(data, self.proc.stdin)
|
pickle.dump(data, self.proc.stdin)
|
||||||
self.proc.stdin.close()
|
self.proc.stdin.close()
|
||||||
@ -136,8 +138,8 @@ class Process(RemoteEventHandler):
|
|||||||
continue
|
continue
|
||||||
else:
|
else:
|
||||||
raise
|
raise
|
||||||
|
|
||||||
RemoteEventHandler.__init__(self, conn, name+'_parent', pid=self.proc.pid, debug=debug)
|
RemoteEventHandler.__init__(self, conn, name+'_parent', pid=self.proc.pid, debug=self.debug)
|
||||||
self.debugMsg('Connected to child process.')
|
self.debugMsg('Connected to child process.')
|
||||||
|
|
||||||
atexit.register(self.join)
|
atexit.register(self.join)
|
||||||
@ -167,10 +169,11 @@ class Process(RemoteEventHandler):
|
|||||||
def startEventLoop(name, port, authkey, ppid, debug=False):
|
def startEventLoop(name, port, authkey, ppid, debug=False):
|
||||||
if debug:
|
if debug:
|
||||||
import os
|
import os
|
||||||
print('[%d] connecting to server at port localhost:%d, authkey=%s..' % (os.getpid(), port, repr(authkey)))
|
cprint.cout(debug, '[%d] connecting to server at port localhost:%d, authkey=%s..\n'
|
||||||
|
% (os.getpid(), port, repr(authkey)), -1)
|
||||||
conn = multiprocessing.connection.Client(('localhost', int(port)), authkey=authkey)
|
conn = multiprocessing.connection.Client(('localhost', int(port)), authkey=authkey)
|
||||||
if debug:
|
if debug:
|
||||||
print('[%d] connected; starting remote proxy.' % os.getpid())
|
cprint.cout(debug, '[%d] connected; starting remote proxy.\n' % os.getpid(), -1)
|
||||||
global HANDLER
|
global HANDLER
|
||||||
#ppid = 0 if not hasattr(os, 'getppid') else os.getppid()
|
#ppid = 0 if not hasattr(os, 'getppid') else os.getppid()
|
||||||
HANDLER = RemoteEventHandler(conn, name, ppid, debug=debug)
|
HANDLER = RemoteEventHandler(conn, name, ppid, debug=debug)
|
||||||
@ -380,17 +383,17 @@ class QtProcess(Process):
|
|||||||
def __init__(self, **kwds):
|
def __init__(self, **kwds):
|
||||||
if 'target' not in kwds:
|
if 'target' not in kwds:
|
||||||
kwds['target'] = startQtEventLoop
|
kwds['target'] = startQtEventLoop
|
||||||
|
from ..Qt import QtGui ## avoid module-level import to keep bootstrap snappy.
|
||||||
self._processRequests = kwds.pop('processRequests', True)
|
self._processRequests = kwds.pop('processRequests', True)
|
||||||
|
if self._processRequests and QtGui.QApplication.instance() is None:
|
||||||
|
raise Exception("Must create QApplication before starting QtProcess, or use QtProcess(processRequests=False)")
|
||||||
Process.__init__(self, **kwds)
|
Process.__init__(self, **kwds)
|
||||||
self.startEventTimer()
|
self.startEventTimer()
|
||||||
|
|
||||||
def startEventTimer(self):
|
def startEventTimer(self):
|
||||||
from ..Qt import QtGui, QtCore ## avoid module-level import to keep bootstrap snappy.
|
from ..Qt import QtCore ## avoid module-level import to keep bootstrap snappy.
|
||||||
self.timer = QtCore.QTimer()
|
self.timer = QtCore.QTimer()
|
||||||
if self._processRequests:
|
if self._processRequests:
|
||||||
app = QtGui.QApplication.instance()
|
|
||||||
if app is None:
|
|
||||||
raise Exception("Must create QApplication before starting QtProcess, or use QtProcess(processRequests=False)")
|
|
||||||
self.startRequestProcessing()
|
self.startRequestProcessing()
|
||||||
|
|
||||||
def startRequestProcessing(self, interval=0.01):
|
def startRequestProcessing(self, interval=0.01):
|
||||||
@ -412,10 +415,10 @@ class QtProcess(Process):
|
|||||||
def startQtEventLoop(name, port, authkey, ppid, debug=False):
|
def startQtEventLoop(name, port, authkey, ppid, debug=False):
|
||||||
if debug:
|
if debug:
|
||||||
import os
|
import os
|
||||||
print('[%d] connecting to server at port localhost:%d, authkey=%s..' % (os.getpid(), port, repr(authkey)))
|
cprint.cout(debug, '[%d] connecting to server at port localhost:%d, authkey=%s..\n' % (os.getpid(), port, repr(authkey)), -1)
|
||||||
conn = multiprocessing.connection.Client(('localhost', int(port)), authkey=authkey)
|
conn = multiprocessing.connection.Client(('localhost', int(port)), authkey=authkey)
|
||||||
if debug:
|
if debug:
|
||||||
print('[%d] connected; starting remote proxy.' % os.getpid())
|
cprint.cout(debug, '[%d] connected; starting remote proxy.\n' % os.getpid(), -1)
|
||||||
from ..Qt import QtGui, QtCore
|
from ..Qt import QtGui, QtCore
|
||||||
#from PyQt4 import QtGui, QtCore
|
#from PyQt4 import QtGui, QtCore
|
||||||
app = QtGui.QApplication.instance()
|
app = QtGui.QApplication.instance()
|
||||||
@ -445,11 +448,13 @@ class FileForwarder(threading.Thread):
|
|||||||
which ensures that the correct behavior is achieved even if
|
which ensures that the correct behavior is achieved even if
|
||||||
sys.stdout/stderr are replaced at runtime.
|
sys.stdout/stderr are replaced at runtime.
|
||||||
"""
|
"""
|
||||||
def __init__(self, input, output):
|
def __init__(self, input, output, color):
|
||||||
threading.Thread.__init__(self)
|
threading.Thread.__init__(self)
|
||||||
self.input = input
|
self.input = input
|
||||||
self.output = output
|
self.output = output
|
||||||
self.lock = threading.Lock()
|
self.lock = threading.Lock()
|
||||||
|
self.daemon = True
|
||||||
|
self.color = color
|
||||||
self.start()
|
self.start()
|
||||||
|
|
||||||
def run(self):
|
def run(self):
|
||||||
@ -457,12 +462,12 @@ class FileForwarder(threading.Thread):
|
|||||||
while True:
|
while True:
|
||||||
line = self.input.readline()
|
line = self.input.readline()
|
||||||
with self.lock:
|
with self.lock:
|
||||||
sys.stdout.write(line)
|
cprint.cout(self.color, line, -1)
|
||||||
elif self.output == 'stderr':
|
elif self.output == 'stderr':
|
||||||
while True:
|
while True:
|
||||||
line = self.input.readline()
|
line = self.input.readline()
|
||||||
with self.lock:
|
with self.lock:
|
||||||
sys.stderr.write(line)
|
cprint.cerr(self.color, line, -1)
|
||||||
else:
|
else:
|
||||||
while True:
|
while True:
|
||||||
line = self.input.readline()
|
line = self.input.readline()
|
||||||
|
@ -7,6 +7,9 @@ except ImportError:
|
|||||||
import builtins
|
import builtins
|
||||||
import pickle
|
import pickle
|
||||||
|
|
||||||
|
# color printing for debugging
|
||||||
|
from ..util import cprint
|
||||||
|
|
||||||
class ClosedError(Exception):
|
class ClosedError(Exception):
|
||||||
"""Raised when an event handler receives a request to close the connection
|
"""Raised when an event handler receives a request to close the connection
|
||||||
or discovers that the connection has been closed."""
|
or discovers that the connection has been closed."""
|
||||||
@ -80,7 +83,7 @@ class RemoteEventHandler(object):
|
|||||||
def debugMsg(self, msg):
|
def debugMsg(self, msg):
|
||||||
if not self.debug:
|
if not self.debug:
|
||||||
return
|
return
|
||||||
print("[%d] %s" % (os.getpid(), str(msg)))
|
cprint.cout(self.debug, "[%d] %s\n" % (os.getpid(), str(msg)), -1)
|
||||||
|
|
||||||
def getProxyOption(self, opt):
|
def getProxyOption(self, opt):
|
||||||
return self.proxyOptions[opt]
|
return self.proxyOptions[opt]
|
||||||
@ -299,23 +302,23 @@ class RemoteEventHandler(object):
|
|||||||
(The docstring has information that is nevertheless useful to the programmer
|
(The docstring has information that is nevertheless useful to the programmer
|
||||||
as it describes the internal protocol used to communicate between processes)
|
as it describes the internal protocol used to communicate between processes)
|
||||||
|
|
||||||
========== ====================================================================
|
============== ====================================================================
|
||||||
Arguments:
|
**Arguments:**
|
||||||
request String describing the type of request being sent (see below)
|
request String describing the type of request being sent (see below)
|
||||||
reqId Integer uniquely linking a result back to the request that generated
|
reqId Integer uniquely linking a result back to the request that generated
|
||||||
it. (most requests leave this blank)
|
it. (most requests leave this blank)
|
||||||
callSync 'sync': return the actual result of the request
|
callSync 'sync': return the actual result of the request
|
||||||
'async': return a Request object which can be used to look up the
|
'async': return a Request object which can be used to look up the
|
||||||
result later
|
result later
|
||||||
'off': return no result
|
'off': return no result
|
||||||
timeout Time in seconds to wait for a response when callSync=='sync'
|
timeout Time in seconds to wait for a response when callSync=='sync'
|
||||||
opts Extra arguments sent to the remote process that determine the way
|
opts Extra arguments sent to the remote process that determine the way
|
||||||
the request will be handled (see below)
|
the request will be handled (see below)
|
||||||
returnType 'proxy', 'value', or 'auto'
|
returnType 'proxy', 'value', or 'auto'
|
||||||
byteData If specified, this is a list of objects to be sent as byte messages
|
byteData If specified, this is a list of objects to be sent as byte messages
|
||||||
to the remote process.
|
to the remote process.
|
||||||
This is used to send large arrays without the cost of pickling.
|
This is used to send large arrays without the cost of pickling.
|
||||||
========== ====================================================================
|
============== ====================================================================
|
||||||
|
|
||||||
Description of request strings and options allowed for each:
|
Description of request strings and options allowed for each:
|
||||||
|
|
||||||
@ -576,7 +579,7 @@ class Request(object):
|
|||||||
return self._result
|
return self._result
|
||||||
|
|
||||||
if timeout is None:
|
if timeout is None:
|
||||||
timeout = self.timeout
|
timeout = self.timeout
|
||||||
|
|
||||||
if block:
|
if block:
|
||||||
start = time.time()
|
start = time.time()
|
||||||
|
@ -36,6 +36,7 @@ class GLViewWidget(QtOpenGL.QGLWidget):
|
|||||||
## (rotation around z-axis 0 points along x-axis)
|
## (rotation around z-axis 0 points along x-axis)
|
||||||
'viewport': None, ## glViewport params; None == whole widget
|
'viewport': None, ## glViewport params; None == whole widget
|
||||||
}
|
}
|
||||||
|
self.setBackgroundColor('k')
|
||||||
self.items = []
|
self.items = []
|
||||||
self.noRepeatKeys = [QtCore.Qt.Key_Right, QtCore.Qt.Key_Left, QtCore.Qt.Key_Up, QtCore.Qt.Key_Down, QtCore.Qt.Key_PageUp, QtCore.Qt.Key_PageDown]
|
self.noRepeatKeys = [QtCore.Qt.Key_Right, QtCore.Qt.Key_Left, QtCore.Qt.Key_Up, QtCore.Qt.Key_Down, QtCore.Qt.Key_PageUp, QtCore.Qt.Key_PageDown]
|
||||||
self.keysPressed = {}
|
self.keysPressed = {}
|
||||||
@ -64,9 +65,16 @@ class GLViewWidget(QtOpenGL.QGLWidget):
|
|||||||
|
|
||||||
|
|
||||||
def initializeGL(self):
|
def initializeGL(self):
|
||||||
glClearColor(0.0, 0.0, 0.0, 0.0)
|
|
||||||
self.resizeGL(self.width(), self.height())
|
self.resizeGL(self.width(), self.height())
|
||||||
|
|
||||||
|
def setBackgroundColor(self, *args, **kwds):
|
||||||
|
"""
|
||||||
|
Set the background color of the widget. Accepts the same arguments as
|
||||||
|
pg.mkColor().
|
||||||
|
"""
|
||||||
|
self.opts['bgcolor'] = fn.mkColor(*args, **kwds)
|
||||||
|
self.update()
|
||||||
|
|
||||||
def getViewport(self):
|
def getViewport(self):
|
||||||
vp = self.opts['viewport']
|
vp = self.opts['viewport']
|
||||||
if vp is None:
|
if vp is None:
|
||||||
@ -129,6 +137,12 @@ class GLViewWidget(QtOpenGL.QGLWidget):
|
|||||||
return tr
|
return tr
|
||||||
|
|
||||||
def itemsAt(self, region=None):
|
def itemsAt(self, region=None):
|
||||||
|
"""
|
||||||
|
Return a list of the items displayed in the region (x, y, w, h)
|
||||||
|
relative to the widget.
|
||||||
|
"""
|
||||||
|
region = (region[0], self.height()-(region[1]+region[3]), region[2], region[3])
|
||||||
|
|
||||||
#buf = np.zeros(100000, dtype=np.uint)
|
#buf = np.zeros(100000, dtype=np.uint)
|
||||||
buf = glSelectBuffer(100000)
|
buf = glSelectBuffer(100000)
|
||||||
try:
|
try:
|
||||||
@ -140,12 +154,12 @@ class GLViewWidget(QtOpenGL.QGLWidget):
|
|||||||
|
|
||||||
finally:
|
finally:
|
||||||
hits = glRenderMode(GL_RENDER)
|
hits = glRenderMode(GL_RENDER)
|
||||||
|
|
||||||
items = [(h.near, h.names[0]) for h in hits]
|
items = [(h.near, h.names[0]) for h in hits]
|
||||||
items.sort(key=lambda i: i[0])
|
items.sort(key=lambda i: i[0])
|
||||||
|
|
||||||
return [self._itemNames[i[1]] for i in items]
|
return [self._itemNames[i[1]] for i in items]
|
||||||
|
|
||||||
def paintGL(self, region=None, viewport=None, useItemNames=False):
|
def paintGL(self, region=None, viewport=None, useItemNames=False):
|
||||||
"""
|
"""
|
||||||
viewport specifies the arguments to glViewport. If None, then we use self.opts['viewport']
|
viewport specifies the arguments to glViewport. If None, then we use self.opts['viewport']
|
||||||
@ -158,6 +172,8 @@ class GLViewWidget(QtOpenGL.QGLWidget):
|
|||||||
glViewport(*viewport)
|
glViewport(*viewport)
|
||||||
self.setProjection(region=region)
|
self.setProjection(region=region)
|
||||||
self.setModelview()
|
self.setModelview()
|
||||||
|
bgcolor = self.opts['bgcolor']
|
||||||
|
glClearColor(bgcolor.red(), bgcolor.green(), bgcolor.blue(), 1.0)
|
||||||
glClear( GL_DEPTH_BUFFER_BIT | GL_COLOR_BUFFER_BIT )
|
glClear( GL_DEPTH_BUFFER_BIT | GL_COLOR_BUFFER_BIT )
|
||||||
self.drawItemTree(useItemNames=useItemNames)
|
self.drawItemTree(useItemNames=useItemNames)
|
||||||
|
|
||||||
@ -180,7 +196,7 @@ class GLViewWidget(QtOpenGL.QGLWidget):
|
|||||||
i.paint()
|
i.paint()
|
||||||
except:
|
except:
|
||||||
from .. import debug
|
from .. import debug
|
||||||
pyqtgraph.debug.printExc()
|
debug.printExc()
|
||||||
msg = "Error while drawing item %s." % str(item)
|
msg = "Error while drawing item %s." % str(item)
|
||||||
ver = glGetString(GL_VERSION)
|
ver = glGetString(GL_VERSION)
|
||||||
if ver is not None:
|
if ver is not None:
|
||||||
@ -294,6 +310,17 @@ class GLViewWidget(QtOpenGL.QGLWidget):
|
|||||||
|
|
||||||
def mouseReleaseEvent(self, ev):
|
def mouseReleaseEvent(self, ev):
|
||||||
pass
|
pass
|
||||||
|
# Example item selection code:
|
||||||
|
#region = (ev.pos().x()-5, ev.pos().y()-5, 10, 10)
|
||||||
|
#print(self.itemsAt(region))
|
||||||
|
|
||||||
|
## debugging code: draw the picking region
|
||||||
|
#glViewport(*self.getViewport())
|
||||||
|
#glClear( GL_DEPTH_BUFFER_BIT | GL_COLOR_BUFFER_BIT )
|
||||||
|
#region = (region[0], self.height()-(region[1]+region[3]), region[2], region[3])
|
||||||
|
#self.paintGL(region=region)
|
||||||
|
#self.swapBuffers()
|
||||||
|
|
||||||
|
|
||||||
def wheelEvent(self, ev):
|
def wheelEvent(self, ev):
|
||||||
if (ev.modifiers() & QtCore.Qt.ControlModifier):
|
if (ev.modifiers() & QtCore.Qt.ControlModifier):
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
from ..Qt import QtGui
|
from pyqtgraph.Qt import QtGui
|
||||||
from .. import functions as fn
|
import pyqtgraph.functions as fn
|
||||||
import numpy as np
|
import numpy as np
|
||||||
|
|
||||||
class MeshData(object):
|
class MeshData(object):
|
||||||
@ -23,18 +23,18 @@ class MeshData(object):
|
|||||||
|
|
||||||
def __init__(self, vertexes=None, faces=None, edges=None, vertexColors=None, faceColors=None):
|
def __init__(self, vertexes=None, faces=None, edges=None, vertexColors=None, faceColors=None):
|
||||||
"""
|
"""
|
||||||
============= =====================================================
|
============== =====================================================
|
||||||
Arguments
|
**Arguments:**
|
||||||
vertexes (Nv, 3) array of vertex coordinates.
|
vertexes (Nv, 3) array of vertex coordinates.
|
||||||
If faces is not specified, then this will instead be
|
If faces is not specified, then this will instead be
|
||||||
interpreted as (Nf, 3, 3) array of coordinates.
|
interpreted as (Nf, 3, 3) array of coordinates.
|
||||||
faces (Nf, 3) array of indexes into the vertex array.
|
faces (Nf, 3) array of indexes into the vertex array.
|
||||||
edges [not available yet]
|
edges [not available yet]
|
||||||
vertexColors (Nv, 4) array of vertex colors.
|
vertexColors (Nv, 4) array of vertex colors.
|
||||||
If faces is not specified, then this will instead be
|
If faces is not specified, then this will instead be
|
||||||
interpreted as (Nf, 3, 4) array of colors.
|
interpreted as (Nf, 3, 4) array of colors.
|
||||||
faceColors (Nf, 4) array of face colors.
|
faceColors (Nf, 4) array of face colors.
|
||||||
============= =====================================================
|
============== =====================================================
|
||||||
|
|
||||||
All arguments are optional.
|
All arguments are optional.
|
||||||
"""
|
"""
|
||||||
@ -84,64 +84,11 @@ class MeshData(object):
|
|||||||
if faceColors is not None:
|
if faceColors is not None:
|
||||||
self.setFaceColors(faceColors)
|
self.setFaceColors(faceColors)
|
||||||
|
|
||||||
#self.setFaces(vertexes=vertexes, faces=faces, vertexColors=vertexColors, faceColors=faceColors)
|
|
||||||
|
|
||||||
|
|
||||||
#def setFaces(self, vertexes=None, faces=None, vertexColors=None, faceColors=None):
|
|
||||||
#"""
|
|
||||||
#Set the faces in this data set.
|
|
||||||
#Data may be provided either as an Nx3x3 array of floats (9 float coordinate values per face)::
|
|
||||||
|
|
||||||
#faces = [ [(x, y, z), (x, y, z), (x, y, z)], ... ]
|
|
||||||
|
|
||||||
#or as an Nx3 array of ints (vertex integers) AND an Mx3 array of floats (3 float coordinate values per vertex)::
|
|
||||||
|
|
||||||
#faces = [ (p1, p2, p3), ... ]
|
|
||||||
#vertexes = [ (x, y, z), ... ]
|
|
||||||
|
|
||||||
#"""
|
|
||||||
#if not isinstance(vertexes, np.ndarray):
|
|
||||||
#vertexes = np.array(vertexes)
|
|
||||||
#if vertexes.dtype != np.float:
|
|
||||||
#vertexes = vertexes.astype(float)
|
|
||||||
#if faces is None:
|
|
||||||
#self._setIndexedFaces(vertexes, vertexColors, faceColors)
|
|
||||||
#else:
|
|
||||||
#self._setUnindexedFaces(faces, vertexes, vertexColors, faceColors)
|
|
||||||
##print self.vertexes().shape
|
|
||||||
##print self.faces().shape
|
|
||||||
|
|
||||||
|
|
||||||
#def setMeshColor(self, color):
|
|
||||||
#"""Set the color of the entire mesh. This removes any per-face or per-vertex colors."""
|
|
||||||
#color = fn.Color(color)
|
|
||||||
#self._meshColor = color.glColor()
|
|
||||||
#self._vertexColors = None
|
|
||||||
#self._faceColors = None
|
|
||||||
|
|
||||||
|
|
||||||
#def __iter__(self):
|
|
||||||
#"""Iterate over all faces, yielding a list of three tuples [(position, normal, color), ...] for each face."""
|
|
||||||
#vnorms = self.vertexNormals()
|
|
||||||
#vcolors = self.vertexColors()
|
|
||||||
#for i in range(self._faces.shape[0]):
|
|
||||||
#face = []
|
|
||||||
#for j in [0,1,2]:
|
|
||||||
#vind = self._faces[i,j]
|
|
||||||
#pos = self._vertexes[vind]
|
|
||||||
#norm = vnorms[vind]
|
|
||||||
#if vcolors is None:
|
|
||||||
#color = self._meshColor
|
|
||||||
#else:
|
|
||||||
#color = vcolors[vind]
|
|
||||||
#face.append((pos, norm, color))
|
|
||||||
#yield face
|
|
||||||
|
|
||||||
#def __len__(self):
|
|
||||||
#return len(self._faces)
|
|
||||||
|
|
||||||
def faces(self):
|
def faces(self):
|
||||||
"""Return an array (Nf, 3) of vertex indexes, three per triangular face in the mesh."""
|
"""Return an array (Nf, 3) of vertex indexes, three per triangular face in the mesh.
|
||||||
|
|
||||||
|
If faces have not been computed for this mesh, the function returns None.
|
||||||
|
"""
|
||||||
return self._faces
|
return self._faces
|
||||||
|
|
||||||
def edges(self):
|
def edges(self):
|
||||||
@ -161,8 +108,6 @@ class MeshData(object):
|
|||||||
self.resetNormals()
|
self.resetNormals()
|
||||||
self._vertexColorsIndexedByFaces = None
|
self._vertexColorsIndexedByFaces = None
|
||||||
self._faceColorsIndexedByFaces = None
|
self._faceColorsIndexedByFaces = None
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def vertexes(self, indexed=None):
|
def vertexes(self, indexed=None):
|
||||||
"""Return an array (N,3) of the positions of vertexes in the mesh.
|
"""Return an array (N,3) of the positions of vertexes in the mesh.
|
||||||
@ -207,7 +152,6 @@ class MeshData(object):
|
|||||||
self._vertexNormalsIndexedByFaces = None
|
self._vertexNormalsIndexedByFaces = None
|
||||||
self._faceNormals = None
|
self._faceNormals = None
|
||||||
self._faceNormalsIndexedByFaces = None
|
self._faceNormalsIndexedByFaces = None
|
||||||
|
|
||||||
|
|
||||||
def hasFaceIndexedData(self):
|
def hasFaceIndexedData(self):
|
||||||
"""Return True if this object already has vertex positions indexed by face"""
|
"""Return True if this object already has vertex positions indexed by face"""
|
||||||
@ -229,7 +173,6 @@ class MeshData(object):
|
|||||||
if v is not None:
|
if v is not None:
|
||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
def faceNormals(self, indexed=None):
|
def faceNormals(self, indexed=None):
|
||||||
"""
|
"""
|
||||||
@ -242,7 +185,6 @@ class MeshData(object):
|
|||||||
v = self.vertexes(indexed='faces')
|
v = self.vertexes(indexed='faces')
|
||||||
self._faceNormals = np.cross(v[:,1]-v[:,0], v[:,2]-v[:,0])
|
self._faceNormals = np.cross(v[:,1]-v[:,0], v[:,2]-v[:,0])
|
||||||
|
|
||||||
|
|
||||||
if indexed is None:
|
if indexed is None:
|
||||||
return self._faceNormals
|
return self._faceNormals
|
||||||
elif indexed == 'faces':
|
elif indexed == 'faces':
|
||||||
@ -266,7 +208,11 @@ class MeshData(object):
|
|||||||
vertFaces = self.vertexFaces()
|
vertFaces = self.vertexFaces()
|
||||||
self._vertexNormals = np.empty(self._vertexes.shape, dtype=float)
|
self._vertexNormals = np.empty(self._vertexes.shape, dtype=float)
|
||||||
for vindex in xrange(self._vertexes.shape[0]):
|
for vindex in xrange(self._vertexes.shape[0]):
|
||||||
norms = faceNorms[vertFaces[vindex]] ## get all face normals
|
faces = vertFaces[vindex]
|
||||||
|
if len(faces) == 0:
|
||||||
|
self._vertexNormals[vindex] = (0,0,0)
|
||||||
|
continue
|
||||||
|
norms = faceNorms[faces] ## get all face normals
|
||||||
norm = norms.sum(axis=0) ## sum normals
|
norm = norms.sum(axis=0) ## sum normals
|
||||||
norm /= (norm**2).sum()**0.5 ## and re-normalize
|
norm /= (norm**2).sum()**0.5 ## and re-normalize
|
||||||
self._vertexNormals[vindex] = norm
|
self._vertexNormals[vindex] = norm
|
||||||
@ -363,7 +309,6 @@ class MeshData(object):
|
|||||||
## This is done by collapsing into a list of 'unique' vertexes (difference < 1e-14)
|
## This is done by collapsing into a list of 'unique' vertexes (difference < 1e-14)
|
||||||
|
|
||||||
## I think generally this should be discouraged..
|
## I think generally this should be discouraged..
|
||||||
|
|
||||||
faces = self._vertexesIndexedByFaces
|
faces = self._vertexesIndexedByFaces
|
||||||
verts = {} ## used to remember the index of each vertex position
|
verts = {} ## used to remember the index of each vertex position
|
||||||
self._faces = np.empty(faces.shape[:2], dtype=np.uint)
|
self._faces = np.empty(faces.shape[:2], dtype=np.uint)
|
||||||
@ -403,12 +348,10 @@ class MeshData(object):
|
|||||||
Return list mapping each vertex index to a list of face indexes that use the vertex.
|
Return list mapping each vertex index to a list of face indexes that use the vertex.
|
||||||
"""
|
"""
|
||||||
if self._vertexFaces is None:
|
if self._vertexFaces is None:
|
||||||
self._vertexFaces = [None] * len(self.vertexes())
|
self._vertexFaces = [[] for i in xrange(len(self.vertexes()))]
|
||||||
for i in xrange(self._faces.shape[0]):
|
for i in xrange(self._faces.shape[0]):
|
||||||
face = self._faces[i]
|
face = self._faces[i]
|
||||||
for ind in face:
|
for ind in face:
|
||||||
if self._vertexFaces[ind] is None:
|
|
||||||
self._vertexFaces[ind] = [] ## need a unique/empty list to fill
|
|
||||||
self._vertexFaces[ind].append(i)
|
self._vertexFaces[ind].append(i)
|
||||||
return self._vertexFaces
|
return self._vertexFaces
|
||||||
|
|
||||||
@ -426,22 +369,35 @@ class MeshData(object):
|
|||||||
#pass
|
#pass
|
||||||
|
|
||||||
def _computeEdges(self):
|
def _computeEdges(self):
|
||||||
## generate self._edges from self._faces
|
if not self.hasFaceIndexedData:
|
||||||
#print self._faces
|
## generate self._edges from self._faces
|
||||||
nf = len(self._faces)
|
nf = len(self._faces)
|
||||||
edges = np.empty(nf*3, dtype=[('i', np.uint, 2)])
|
edges = np.empty(nf*3, dtype=[('i', np.uint, 2)])
|
||||||
edges['i'][0:nf] = self._faces[:,:2]
|
edges['i'][0:nf] = self._faces[:,:2]
|
||||||
edges['i'][nf:2*nf] = self._faces[:,1:3]
|
edges['i'][nf:2*nf] = self._faces[:,1:3]
|
||||||
edges['i'][-nf:,0] = self._faces[:,2]
|
edges['i'][-nf:,0] = self._faces[:,2]
|
||||||
edges['i'][-nf:,1] = self._faces[:,0]
|
edges['i'][-nf:,1] = self._faces[:,0]
|
||||||
|
|
||||||
# sort per-edge
|
# sort per-edge
|
||||||
mask = edges['i'][:,0] > edges['i'][:,1]
|
mask = edges['i'][:,0] > edges['i'][:,1]
|
||||||
edges['i'][mask] = edges['i'][mask][:,::-1]
|
edges['i'][mask] = edges['i'][mask][:,::-1]
|
||||||
|
|
||||||
# remove duplicate entries
|
# remove duplicate entries
|
||||||
self._edges = np.unique(edges)['i']
|
self._edges = np.unique(edges)['i']
|
||||||
#print self._edges
|
#print self._edges
|
||||||
|
elif self._vertexesIndexedByFaces is not None:
|
||||||
|
verts = self._vertexesIndexedByFaces
|
||||||
|
edges = np.empty((verts.shape[0], 3, 2), dtype=np.uint)
|
||||||
|
nf = verts.shape[0]
|
||||||
|
edges[:,0,0] = np.arange(nf) * 3
|
||||||
|
edges[:,0,1] = edges[:,0,0] + 1
|
||||||
|
edges[:,1,0] = edges[:,0,1]
|
||||||
|
edges[:,1,1] = edges[:,1,0] + 1
|
||||||
|
edges[:,2,0] = edges[:,1,1]
|
||||||
|
edges[:,2,1] = edges[:,0,0]
|
||||||
|
self._edges = edges
|
||||||
|
else:
|
||||||
|
raise Exception("MeshData cannot generate edges--no faces in this data.")
|
||||||
|
|
||||||
|
|
||||||
def save(self):
|
def save(self):
|
||||||
@ -516,4 +472,33 @@ class MeshData(object):
|
|||||||
|
|
||||||
return MeshData(vertexes=verts, faces=faces)
|
return MeshData(vertexes=verts, faces=faces)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def cylinder(rows, cols, radius=[1.0, 1.0], length=1.0, offset=False):
|
||||||
|
"""
|
||||||
|
Return a MeshData instance with vertexes and faces computed
|
||||||
|
for a cylindrical surface.
|
||||||
|
The cylinder may be tapered with different radii at each end (truncated cone)
|
||||||
|
"""
|
||||||
|
verts = np.empty((rows+1, cols, 3), dtype=float)
|
||||||
|
if isinstance(radius, int):
|
||||||
|
radius = [radius, radius] # convert to list
|
||||||
|
## compute vertexes
|
||||||
|
th = np.linspace(2 * np.pi, 0, cols).reshape(1, cols)
|
||||||
|
r = np.linspace(radius[0],radius[1],num=rows+1, endpoint=True).reshape(rows+1, 1) # radius as a function of z
|
||||||
|
verts[...,2] = np.linspace(0, length, num=rows+1, endpoint=True).reshape(rows+1, 1) # z
|
||||||
|
if offset:
|
||||||
|
th = th + ((np.pi / cols) * np.arange(rows+1).reshape(rows+1,1)) ## rotate each row by 1/2 column
|
||||||
|
verts[...,0] = r * np.cos(th) # x = r cos(th)
|
||||||
|
verts[...,1] = r * np.sin(th) # y = r sin(th)
|
||||||
|
verts = verts.reshape((rows+1)*cols, 3) # just reshape: no redundant vertices...
|
||||||
|
## compute faces
|
||||||
|
faces = np.empty((rows*cols*2, 3), dtype=np.uint)
|
||||||
|
rowtemplate1 = ((np.arange(cols).reshape(cols, 1) + np.array([[0, 1, 0]])) % cols) + np.array([[0, 0, cols]])
|
||||||
|
rowtemplate2 = ((np.arange(cols).reshape(cols, 1) + np.array([[0, 1, 1]])) % cols) + np.array([[cols, 0, cols]])
|
||||||
|
for row in range(rows):
|
||||||
|
start = row * cols * 2
|
||||||
|
faces[start:start+cols] = rowtemplate1 + row * cols
|
||||||
|
faces[start+cols:start+(cols*2)] = rowtemplate2 + row * cols
|
||||||
|
|
||||||
|
return MeshData(vertexes=verts, faces=faces)
|
||||||
|
|
@ -45,7 +45,7 @@ class GLAxisItem(GLGraphicsItem):
|
|||||||
|
|
||||||
if self.antialias:
|
if self.antialias:
|
||||||
glEnable(GL_LINE_SMOOTH)
|
glEnable(GL_LINE_SMOOTH)
|
||||||
glHint(GL_LINE_SMOOTH_HINT, GL_NICEST);
|
glHint(GL_LINE_SMOOTH_HINT, GL_NICEST)
|
||||||
|
|
||||||
glBegin( GL_LINES )
|
glBegin( GL_LINES )
|
||||||
|
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
import numpy as np
|
||||||
|
|
||||||
from OpenGL.GL import *
|
from OpenGL.GL import *
|
||||||
from .. GLGraphicsItem import GLGraphicsItem
|
from .. GLGraphicsItem import GLGraphicsItem
|
||||||
from ... import QtGui
|
from ... import QtGui
|
||||||
@ -16,8 +18,9 @@ class GLGridItem(GLGraphicsItem):
|
|||||||
self.setGLOptions(glOptions)
|
self.setGLOptions(glOptions)
|
||||||
self.antialias = antialias
|
self.antialias = antialias
|
||||||
if size is None:
|
if size is None:
|
||||||
size = QtGui.QVector3D(1,1,1)
|
size = QtGui.QVector3D(20,20,1)
|
||||||
self.setSize(size=size)
|
self.setSize(size=size)
|
||||||
|
self.setSpacing(1, 1, 1)
|
||||||
|
|
||||||
def setSize(self, x=None, y=None, z=None, size=None):
|
def setSize(self, x=None, y=None, z=None, size=None):
|
||||||
"""
|
"""
|
||||||
@ -33,8 +36,22 @@ class GLGridItem(GLGraphicsItem):
|
|||||||
|
|
||||||
def size(self):
|
def size(self):
|
||||||
return self.__size[:]
|
return self.__size[:]
|
||||||
|
|
||||||
|
def setSpacing(self, x=None, y=None, z=None, spacing=None):
|
||||||
|
"""
|
||||||
|
Set the spacing between grid lines.
|
||||||
|
Arguments can be x,y,z or spacing=QVector3D().
|
||||||
|
"""
|
||||||
|
if spacing is not None:
|
||||||
|
x = spacing.x()
|
||||||
|
y = spacing.y()
|
||||||
|
z = spacing.z()
|
||||||
|
self.__spacing = [x,y,z]
|
||||||
|
self.update()
|
||||||
|
|
||||||
|
def spacing(self):
|
||||||
|
return self.__spacing[:]
|
||||||
|
|
||||||
def paint(self):
|
def paint(self):
|
||||||
self.setupGLState()
|
self.setupGLState()
|
||||||
|
|
||||||
@ -42,17 +59,20 @@ class GLGridItem(GLGraphicsItem):
|
|||||||
glEnable(GL_LINE_SMOOTH)
|
glEnable(GL_LINE_SMOOTH)
|
||||||
glEnable(GL_BLEND)
|
glEnable(GL_BLEND)
|
||||||
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA)
|
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA)
|
||||||
glHint(GL_LINE_SMOOTH_HINT, GL_NICEST);
|
glHint(GL_LINE_SMOOTH_HINT, GL_NICEST)
|
||||||
|
|
||||||
glBegin( GL_LINES )
|
glBegin( GL_LINES )
|
||||||
|
|
||||||
x,y,z = self.size()
|
x,y,z = self.size()
|
||||||
|
xs,ys,zs = self.spacing()
|
||||||
|
xvals = np.arange(-x/2., x/2. + xs*0.001, xs)
|
||||||
|
yvals = np.arange(-y/2., y/2. + ys*0.001, ys)
|
||||||
glColor4f(1, 1, 1, .3)
|
glColor4f(1, 1, 1, .3)
|
||||||
for x in range(-10, 11):
|
for x in xvals:
|
||||||
glVertex3f(x, -10, 0)
|
glVertex3f(x, yvals[0], 0)
|
||||||
glVertex3f(x, 10, 0)
|
glVertex3f(x, yvals[-1], 0)
|
||||||
for y in range(-10, 11):
|
for y in yvals:
|
||||||
glVertex3f(-10, y, 0)
|
glVertex3f(xvals[0], y, 0)
|
||||||
glVertex3f( 10, y, 0)
|
glVertex3f(xvals[-1], y, 0)
|
||||||
|
|
||||||
glEnd()
|
glEnd()
|
||||||
|
@ -16,6 +16,7 @@ class GLLinePlotItem(GLGraphicsItem):
|
|||||||
glopts = kwds.pop('glOptions', 'additive')
|
glopts = kwds.pop('glOptions', 'additive')
|
||||||
self.setGLOptions(glopts)
|
self.setGLOptions(glopts)
|
||||||
self.pos = None
|
self.pos = None
|
||||||
|
self.mode = 'line_strip'
|
||||||
self.width = 1.
|
self.width = 1.
|
||||||
self.color = (1.0,1.0,1.0,1.0)
|
self.color = (1.0,1.0,1.0,1.0)
|
||||||
self.setData(**kwds)
|
self.setData(**kwds)
|
||||||
@ -27,7 +28,7 @@ class GLLinePlotItem(GLGraphicsItem):
|
|||||||
colors unchanged, etc.
|
colors unchanged, etc.
|
||||||
|
|
||||||
==================== ==================================================
|
==================== ==================================================
|
||||||
Arguments:
|
**Arguments:**
|
||||||
------------------------------------------------------------------------
|
------------------------------------------------------------------------
|
||||||
pos (N,3) array of floats specifying point locations.
|
pos (N,3) array of floats specifying point locations.
|
||||||
color (N,4) array of floats (0.0-1.0) or
|
color (N,4) array of floats (0.0-1.0) or
|
||||||
@ -35,9 +36,13 @@ class GLLinePlotItem(GLGraphicsItem):
|
|||||||
a single color for the entire item.
|
a single color for the entire item.
|
||||||
width float specifying line width
|
width float specifying line width
|
||||||
antialias enables smooth line drawing
|
antialias enables smooth line drawing
|
||||||
|
mode 'lines': Each pair of vertexes draws a single line
|
||||||
|
segment.
|
||||||
|
'line_strip': All vertexes are drawn as a
|
||||||
|
continuous set of line segments.
|
||||||
==================== ==================================================
|
==================== ==================================================
|
||||||
"""
|
"""
|
||||||
args = ['pos', 'color', 'width', 'connected', 'antialias']
|
args = ['pos', 'color', 'width', 'mode', 'antialias']
|
||||||
for k in kwds.keys():
|
for k in kwds.keys():
|
||||||
if k not in args:
|
if k not in args:
|
||||||
raise Exception('Invalid keyword argument: %s (allowed arguments are %s)' % (k, str(args)))
|
raise Exception('Invalid keyword argument: %s (allowed arguments are %s)' % (k, str(args)))
|
||||||
@ -91,9 +96,15 @@ class GLLinePlotItem(GLGraphicsItem):
|
|||||||
glEnable(GL_LINE_SMOOTH)
|
glEnable(GL_LINE_SMOOTH)
|
||||||
glEnable(GL_BLEND)
|
glEnable(GL_BLEND)
|
||||||
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA)
|
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA)
|
||||||
glHint(GL_LINE_SMOOTH_HINT, GL_NICEST);
|
glHint(GL_LINE_SMOOTH_HINT, GL_NICEST)
|
||||||
|
|
||||||
|
if self.mode == 'line_strip':
|
||||||
|
glDrawArrays(GL_LINE_STRIP, 0, int(self.pos.size / self.pos.shape[-1]))
|
||||||
|
elif self.mode == 'lines':
|
||||||
|
glDrawArrays(GL_LINES, 0, int(self.pos.size / self.pos.shape[-1]))
|
||||||
|
else:
|
||||||
|
raise Exception("Unknown line mode '%s'. (must be 'lines' or 'line_strip')" % self.mode)
|
||||||
|
|
||||||
glDrawArrays(GL_LINE_STRIP, 0, int(self.pos.size / self.pos.shape[-1]))
|
|
||||||
finally:
|
finally:
|
||||||
glDisableClientState(GL_COLOR_ARRAY)
|
glDisableClientState(GL_COLOR_ARRAY)
|
||||||
glDisableClientState(GL_VERTEX_ARRAY)
|
glDisableClientState(GL_VERTEX_ARRAY)
|
||||||
|
@ -19,7 +19,7 @@ class GLMeshItem(GLGraphicsItem):
|
|||||||
def __init__(self, **kwds):
|
def __init__(self, **kwds):
|
||||||
"""
|
"""
|
||||||
============== =====================================================
|
============== =====================================================
|
||||||
Arguments
|
**Arguments:**
|
||||||
meshdata MeshData object from which to determine geometry for
|
meshdata MeshData object from which to determine geometry for
|
||||||
this item.
|
this item.
|
||||||
color Default face color used if no vertex or face colors
|
color Default face color used if no vertex or face colors
|
||||||
@ -153,8 +153,12 @@ class GLMeshItem(GLGraphicsItem):
|
|||||||
self.colors = md.faceColors(indexed='faces')
|
self.colors = md.faceColors(indexed='faces')
|
||||||
|
|
||||||
if self.opts['drawEdges']:
|
if self.opts['drawEdges']:
|
||||||
self.edges = md.edges()
|
if not md.hasFaceIndexedData():
|
||||||
self.edgeVerts = md.vertexes()
|
self.edges = md.edges()
|
||||||
|
self.edgeVerts = md.vertexes()
|
||||||
|
else:
|
||||||
|
self.edges = md.edges()
|
||||||
|
self.edgeVerts = md.vertexes(indexed='faces')
|
||||||
return
|
return
|
||||||
|
|
||||||
def paint(self):
|
def paint(self):
|
||||||
|
@ -28,8 +28,7 @@ class GLScatterPlotItem(GLGraphicsItem):
|
|||||||
colors unchanged, etc.
|
colors unchanged, etc.
|
||||||
|
|
||||||
==================== ==================================================
|
==================== ==================================================
|
||||||
Arguments:
|
**Arguments:**
|
||||||
------------------------------------------------------------------------
|
|
||||||
pos (N,3) array of floats specifying point locations.
|
pos (N,3) array of floats specifying point locations.
|
||||||
color (N,4) array of floats (0.0-1.0) specifying
|
color (N,4) array of floats (0.0-1.0) specifying
|
||||||
spot colors OR a tuple of floats specifying
|
spot colors OR a tuple of floats specifying
|
||||||
|
@ -36,14 +36,14 @@ class GLSurfacePlotItem(GLMeshItem):
|
|||||||
"""
|
"""
|
||||||
Update the data in this surface plot.
|
Update the data in this surface plot.
|
||||||
|
|
||||||
========== =====================================================================
|
============== =====================================================================
|
||||||
Arguments
|
**Arguments:**
|
||||||
x,y 1D arrays of values specifying the x,y positions of vertexes in the
|
x,y 1D arrays of values specifying the x,y positions of vertexes in the
|
||||||
grid. If these are omitted, then the values will be assumed to be
|
grid. If these are omitted, then the values will be assumed to be
|
||||||
integers.
|
integers.
|
||||||
z 2D array of height values for each grid vertex.
|
z 2D array of height values for each grid vertex.
|
||||||
colors (width, height, 4) array of vertex colors.
|
colors (width, height, 4) array of vertex colors.
|
||||||
========== =====================================================================
|
============== =====================================================================
|
||||||
|
|
||||||
All arguments are optional.
|
All arguments are optional.
|
||||||
|
|
||||||
|
254
ordereddict.py
254
ordereddict.py
@ -1,127 +1,127 @@
|
|||||||
# Copyright (c) 2009 Raymond Hettinger
|
# Copyright (c) 2009 Raymond Hettinger
|
||||||
#
|
#
|
||||||
# Permission is hereby granted, free of charge, to any person
|
# Permission is hereby granted, free of charge, to any person
|
||||||
# obtaining a copy of this software and associated documentation files
|
# obtaining a copy of this software and associated documentation files
|
||||||
# (the "Software"), to deal in the Software without restriction,
|
# (the "Software"), to deal in the Software without restriction,
|
||||||
# including without limitation the rights to use, copy, modify, merge,
|
# including without limitation the rights to use, copy, modify, merge,
|
||||||
# publish, distribute, sublicense, and/or sell copies of the Software,
|
# publish, distribute, sublicense, and/or sell copies of the Software,
|
||||||
# and to permit persons to whom the Software is furnished to do so,
|
# and to permit persons to whom the Software is furnished to do so,
|
||||||
# subject to the following conditions:
|
# subject to the following conditions:
|
||||||
#
|
#
|
||||||
# The above copyright notice and this permission notice shall be
|
# The above copyright notice and this permission notice shall be
|
||||||
# included in all copies or substantial portions of the Software.
|
# included in all copies or substantial portions of the Software.
|
||||||
#
|
#
|
||||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||||
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
|
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
|
||||||
# OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
# OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||||
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
|
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
|
||||||
# HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
# HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||||
# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||||
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
||||||
# OTHER DEALINGS IN THE SOFTWARE.
|
# OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
|
||||||
from UserDict import DictMixin
|
from UserDict import DictMixin
|
||||||
|
|
||||||
class OrderedDict(dict, DictMixin):
|
class OrderedDict(dict, DictMixin):
|
||||||
|
|
||||||
def __init__(self, *args, **kwds):
|
def __init__(self, *args, **kwds):
|
||||||
if len(args) > 1:
|
if len(args) > 1:
|
||||||
raise TypeError('expected at most 1 arguments, got %d' % len(args))
|
raise TypeError('expected at most 1 arguments, got %d' % len(args))
|
||||||
try:
|
try:
|
||||||
self.__end
|
self.__end
|
||||||
except AttributeError:
|
except AttributeError:
|
||||||
self.clear()
|
self.clear()
|
||||||
self.update(*args, **kwds)
|
self.update(*args, **kwds)
|
||||||
|
|
||||||
def clear(self):
|
def clear(self):
|
||||||
self.__end = end = []
|
self.__end = end = []
|
||||||
end += [None, end, end] # sentinel node for doubly linked list
|
end += [None, end, end] # sentinel node for doubly linked list
|
||||||
self.__map = {} # key --> [key, prev, next]
|
self.__map = {} # key --> [key, prev, next]
|
||||||
dict.clear(self)
|
dict.clear(self)
|
||||||
|
|
||||||
def __setitem__(self, key, value):
|
def __setitem__(self, key, value):
|
||||||
if key not in self:
|
if key not in self:
|
||||||
end = self.__end
|
end = self.__end
|
||||||
curr = end[1]
|
curr = end[1]
|
||||||
curr[2] = end[1] = self.__map[key] = [key, curr, end]
|
curr[2] = end[1] = self.__map[key] = [key, curr, end]
|
||||||
dict.__setitem__(self, key, value)
|
dict.__setitem__(self, key, value)
|
||||||
|
|
||||||
def __delitem__(self, key):
|
def __delitem__(self, key):
|
||||||
dict.__delitem__(self, key)
|
dict.__delitem__(self, key)
|
||||||
key, prev, next = self.__map.pop(key)
|
key, prev, next = self.__map.pop(key)
|
||||||
prev[2] = next
|
prev[2] = next
|
||||||
next[1] = prev
|
next[1] = prev
|
||||||
|
|
||||||
def __iter__(self):
|
def __iter__(self):
|
||||||
end = self.__end
|
end = self.__end
|
||||||
curr = end[2]
|
curr = end[2]
|
||||||
while curr is not end:
|
while curr is not end:
|
||||||
yield curr[0]
|
yield curr[0]
|
||||||
curr = curr[2]
|
curr = curr[2]
|
||||||
|
|
||||||
def __reversed__(self):
|
def __reversed__(self):
|
||||||
end = self.__end
|
end = self.__end
|
||||||
curr = end[1]
|
curr = end[1]
|
||||||
while curr is not end:
|
while curr is not end:
|
||||||
yield curr[0]
|
yield curr[0]
|
||||||
curr = curr[1]
|
curr = curr[1]
|
||||||
|
|
||||||
def popitem(self, last=True):
|
def popitem(self, last=True):
|
||||||
if not self:
|
if not self:
|
||||||
raise KeyError('dictionary is empty')
|
raise KeyError('dictionary is empty')
|
||||||
if last:
|
if last:
|
||||||
key = reversed(self).next()
|
key = reversed(self).next()
|
||||||
else:
|
else:
|
||||||
key = iter(self).next()
|
key = iter(self).next()
|
||||||
value = self.pop(key)
|
value = self.pop(key)
|
||||||
return key, value
|
return key, value
|
||||||
|
|
||||||
def __reduce__(self):
|
def __reduce__(self):
|
||||||
items = [[k, self[k]] for k in self]
|
items = [[k, self[k]] for k in self]
|
||||||
tmp = self.__map, self.__end
|
tmp = self.__map, self.__end
|
||||||
del self.__map, self.__end
|
del self.__map, self.__end
|
||||||
inst_dict = vars(self).copy()
|
inst_dict = vars(self).copy()
|
||||||
self.__map, self.__end = tmp
|
self.__map, self.__end = tmp
|
||||||
if inst_dict:
|
if inst_dict:
|
||||||
return (self.__class__, (items,), inst_dict)
|
return (self.__class__, (items,), inst_dict)
|
||||||
return self.__class__, (items,)
|
return self.__class__, (items,)
|
||||||
|
|
||||||
def keys(self):
|
def keys(self):
|
||||||
return list(self)
|
return list(self)
|
||||||
|
|
||||||
setdefault = DictMixin.setdefault
|
setdefault = DictMixin.setdefault
|
||||||
update = DictMixin.update
|
update = DictMixin.update
|
||||||
pop = DictMixin.pop
|
pop = DictMixin.pop
|
||||||
values = DictMixin.values
|
values = DictMixin.values
|
||||||
items = DictMixin.items
|
items = DictMixin.items
|
||||||
iterkeys = DictMixin.iterkeys
|
iterkeys = DictMixin.iterkeys
|
||||||
itervalues = DictMixin.itervalues
|
itervalues = DictMixin.itervalues
|
||||||
iteritems = DictMixin.iteritems
|
iteritems = DictMixin.iteritems
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
if not self:
|
if not self:
|
||||||
return '%s()' % (self.__class__.__name__,)
|
return '%s()' % (self.__class__.__name__,)
|
||||||
return '%s(%r)' % (self.__class__.__name__, self.items())
|
return '%s(%r)' % (self.__class__.__name__, self.items())
|
||||||
|
|
||||||
def copy(self):
|
def copy(self):
|
||||||
return self.__class__(self)
|
return self.__class__(self)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def fromkeys(cls, iterable, value=None):
|
def fromkeys(cls, iterable, value=None):
|
||||||
d = cls()
|
d = cls()
|
||||||
for key in iterable:
|
for key in iterable:
|
||||||
d[key] = value
|
d[key] = value
|
||||||
return d
|
return d
|
||||||
|
|
||||||
def __eq__(self, other):
|
def __eq__(self, other):
|
||||||
if isinstance(other, OrderedDict):
|
if isinstance(other, OrderedDict):
|
||||||
if len(self) != len(other):
|
if len(self) != len(other):
|
||||||
return False
|
return False
|
||||||
for p, q in zip(self.items(), other.items()):
|
for p, q in zip(self.items(), other.items()):
|
||||||
if p != q:
|
if p != q:
|
||||||
return False
|
return False
|
||||||
return True
|
return True
|
||||||
return dict.__eq__(self, other)
|
return dict.__eq__(self, other)
|
||||||
|
|
||||||
def __ne__(self, other):
|
def __ne__(self, other):
|
||||||
return not self == other
|
return not self == other
|
||||||
|
@ -107,33 +107,33 @@ class Parameter(QtCore.QObject):
|
|||||||
Parameter instance, the options available to this method are also allowed
|
Parameter instance, the options available to this method are also allowed
|
||||||
by most Parameter subclasses.
|
by most Parameter subclasses.
|
||||||
|
|
||||||
================= =========================================================
|
======================= =========================================================
|
||||||
Keyword Arguments
|
**Keyword Arguments:**
|
||||||
name The name to give this Parameter. This is the name that
|
name The name to give this Parameter. This is the name that
|
||||||
will appear in the left-most column of a ParameterTree
|
will appear in the left-most column of a ParameterTree
|
||||||
for this Parameter.
|
for this Parameter.
|
||||||
value The value to initially assign to this Parameter.
|
value The value to initially assign to this Parameter.
|
||||||
default The default value for this Parameter (most Parameters
|
default The default value for this Parameter (most Parameters
|
||||||
provide an option to 'reset to default').
|
provide an option to 'reset to default').
|
||||||
children A list of children for this Parameter. Children
|
children A list of children for this Parameter. Children
|
||||||
may be given either as a Parameter instance or as a
|
may be given either as a Parameter instance or as a
|
||||||
dictionary to pass to Parameter.create(). In this way,
|
dictionary to pass to Parameter.create(). In this way,
|
||||||
it is possible to specify complex hierarchies of
|
it is possible to specify complex hierarchies of
|
||||||
Parameters from a single nested data structure.
|
Parameters from a single nested data structure.
|
||||||
readonly If True, the user will not be allowed to edit this
|
readonly If True, the user will not be allowed to edit this
|
||||||
Parameter. (default=False)
|
Parameter. (default=False)
|
||||||
enabled If False, any widget(s) for this parameter will appear
|
enabled If False, any widget(s) for this parameter will appear
|
||||||
disabled. (default=True)
|
disabled. (default=True)
|
||||||
visible If False, the Parameter will not appear when displayed
|
visible If False, the Parameter will not appear when displayed
|
||||||
in a ParameterTree. (default=True)
|
in a ParameterTree. (default=True)
|
||||||
renamable If True, the user may rename this Parameter.
|
renamable If True, the user may rename this Parameter.
|
||||||
(default=False)
|
(default=False)
|
||||||
removable If True, the user may remove this Parameter.
|
removable If True, the user may remove this Parameter.
|
||||||
(default=False)
|
(default=False)
|
||||||
expanded If True, the Parameter will appear expanded when
|
expanded If True, the Parameter will appear expanded when
|
||||||
displayed in a ParameterTree (its children will be
|
displayed in a ParameterTree (its children will be
|
||||||
visible). (default=True)
|
visible). (default=True)
|
||||||
================= =========================================================
|
======================= =========================================================
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
@ -516,7 +516,7 @@ class Parameter(QtCore.QObject):
|
|||||||
self.sigChildRemoved.emit(self, child)
|
self.sigChildRemoved.emit(self, child)
|
||||||
try:
|
try:
|
||||||
child.sigTreeStateChanged.disconnect(self.treeStateChanged)
|
child.sigTreeStateChanged.disconnect(self.treeStateChanged)
|
||||||
except TypeError: ## already disconnected
|
except (TypeError, RuntimeError): ## already disconnected
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def clearChildren(self):
|
def clearChildren(self):
|
||||||
@ -675,13 +675,13 @@ class Parameter(QtCore.QObject):
|
|||||||
"""
|
"""
|
||||||
Called when the state of any sub-parameter has changed.
|
Called when the state of any sub-parameter has changed.
|
||||||
|
|
||||||
========== ================================================================
|
============== ================================================================
|
||||||
Arguments:
|
**Arguments:**
|
||||||
param The immediate child whose tree state has changed.
|
param The immediate child whose tree state has changed.
|
||||||
note that the change may have originated from a grandchild.
|
note that the change may have originated from a grandchild.
|
||||||
changes List of tuples describing all changes that have been made
|
changes List of tuples describing all changes that have been made
|
||||||
in this event: (param, changeDescr, data)
|
in this event: (param, changeDescr, data)
|
||||||
========== ================================================================
|
============== ================================================================
|
||||||
|
|
||||||
This function can be extended to react to tree state changes.
|
This function can be extended to react to tree state changes.
|
||||||
"""
|
"""
|
||||||
|
@ -18,16 +18,16 @@ class WidgetParameterItem(ParameterItem):
|
|||||||
* simple widget for editing value (displayed instead of label when item is selected)
|
* simple widget for editing value (displayed instead of label when item is selected)
|
||||||
* button that resets value to default
|
* button that resets value to default
|
||||||
|
|
||||||
================= =============================================================
|
========================== =============================================================
|
||||||
Registered Types:
|
**Registered Types:**
|
||||||
int Displays a :class:`SpinBox <pyqtgraph.SpinBox>` in integer
|
int Displays a :class:`SpinBox <pyqtgraph.SpinBox>` in integer
|
||||||
mode.
|
mode.
|
||||||
float Displays a :class:`SpinBox <pyqtgraph.SpinBox>`.
|
float Displays a :class:`SpinBox <pyqtgraph.SpinBox>`.
|
||||||
bool Displays a QCheckBox
|
bool Displays a QCheckBox
|
||||||
str Displays a QLineEdit
|
str Displays a QLineEdit
|
||||||
color Displays a :class:`ColorButton <pyqtgraph.ColorButton>`
|
color Displays a :class:`ColorButton <pyqtgraph.ColorButton>`
|
||||||
colormap Displays a :class:`GradientWidget <pyqtgraph.GradientWidget>`
|
colormap Displays a :class:`GradientWidget <pyqtgraph.GradientWidget>`
|
||||||
================= =============================================================
|
========================== =============================================================
|
||||||
|
|
||||||
This class can be subclassed by overriding makeWidget() to provide a custom widget.
|
This class can be subclassed by overriding makeWidget() to provide a custom widget.
|
||||||
"""
|
"""
|
||||||
@ -208,12 +208,14 @@ class WidgetParameterItem(ParameterItem):
|
|||||||
val = self.widget.value()
|
val = self.widget.value()
|
||||||
newVal = self.param.setValue(val)
|
newVal = self.param.setValue(val)
|
||||||
|
|
||||||
def widgetValueChanging(self):
|
def widgetValueChanging(self, *args):
|
||||||
"""
|
"""
|
||||||
Called when the widget's value is changing, but not finalized.
|
Called when the widget's value is changing, but not finalized.
|
||||||
For example: editing text before pressing enter or changing focus.
|
For example: editing text before pressing enter or changing focus.
|
||||||
"""
|
"""
|
||||||
pass
|
# This is a bit sketchy: assume the last argument of each signal is
|
||||||
|
# the value..
|
||||||
|
self.param.sigValueChanging.emit(self.param, args[-1])
|
||||||
|
|
||||||
def selected(self, sel):
|
def selected(self, sel):
|
||||||
"""Called when this item has been selected (sel=True) OR deselected (sel=False)"""
|
"""Called when this item has been selected (sel=True) OR deselected (sel=False)"""
|
||||||
|
@ -1,26 +1,26 @@
|
|||||||
"""
|
"""
|
||||||
Allows easy loading of pixmaps used in UI elements.
|
Allows easy loading of pixmaps used in UI elements.
|
||||||
Provides support for frozen environments as well.
|
Provides support for frozen environments as well.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import os, sys, pickle
|
import os, sys, pickle
|
||||||
from ..functions import makeQImage
|
from ..functions import makeQImage
|
||||||
from ..Qt import QtGui
|
from ..Qt import QtGui
|
||||||
if sys.version_info[0] == 2:
|
if sys.version_info[0] == 2:
|
||||||
from . import pixmapData_2 as pixmapData
|
from . import pixmapData_2 as pixmapData
|
||||||
else:
|
else:
|
||||||
from . import pixmapData_3 as pixmapData
|
from . import pixmapData_3 as pixmapData
|
||||||
|
|
||||||
|
|
||||||
def getPixmap(name):
|
def getPixmap(name):
|
||||||
"""
|
"""
|
||||||
Return a QPixmap corresponding to the image file with the given name.
|
Return a QPixmap corresponding to the image file with the given name.
|
||||||
(eg. getPixmap('auto') loads pyqtgraph/pixmaps/auto.png)
|
(eg. getPixmap('auto') loads pyqtgraph/pixmaps/auto.png)
|
||||||
"""
|
"""
|
||||||
key = name+'.png'
|
key = name+'.png'
|
||||||
data = pixmapData.pixmapData[key]
|
data = pixmapData.pixmapData[key]
|
||||||
if isinstance(data, basestring) or isinstance(data, bytes):
|
if isinstance(data, basestring) or isinstance(data, bytes):
|
||||||
pixmapData.pixmapData[key] = pickle.loads(data)
|
pixmapData.pixmapData[key] = pickle.loads(data)
|
||||||
arr = pixmapData.pixmapData[key]
|
arr = pixmapData.pixmapData[key]
|
||||||
return QtGui.QPixmap(makeQImage(arr, alpha=True))
|
return QtGui.QPixmap(makeQImage(arr, alpha=True))
|
||||||
|
|
||||||
|
@ -1,19 +1,19 @@
|
|||||||
import numpy as np
|
import numpy as np
|
||||||
from PyQt4 import QtGui
|
from PyQt4 import QtGui
|
||||||
import os, pickle, sys
|
import os, pickle, sys
|
||||||
|
|
||||||
path = os.path.abspath(os.path.split(__file__)[0])
|
path = os.path.abspath(os.path.split(__file__)[0])
|
||||||
pixmaps = {}
|
pixmaps = {}
|
||||||
for f in os.listdir(path):
|
for f in os.listdir(path):
|
||||||
if not f.endswith('.png'):
|
if not f.endswith('.png'):
|
||||||
continue
|
continue
|
||||||
print(f)
|
print(f)
|
||||||
img = QtGui.QImage(os.path.join(path, f))
|
img = QtGui.QImage(os.path.join(path, f))
|
||||||
ptr = img.bits()
|
ptr = img.bits()
|
||||||
ptr.setsize(img.byteCount())
|
ptr.setsize(img.byteCount())
|
||||||
arr = np.asarray(ptr).reshape(img.height(), img.width(), 4).transpose(1,0,2)
|
arr = np.asarray(ptr).reshape(img.height(), img.width(), 4).transpose(1,0,2)
|
||||||
pixmaps[f] = pickle.dumps(arr)
|
pixmaps[f] = pickle.dumps(arr)
|
||||||
ver = sys.version_info[0]
|
ver = sys.version_info[0]
|
||||||
fh = open(os.path.join(path, 'pixmapData_%d.py' %ver), 'w')
|
fh = open(os.path.join(path, 'pixmapData_%d.py' %ver), 'w')
|
||||||
fh.write("import numpy as np; pixmapData=%s" % repr(pixmaps))
|
fh.write("import numpy as np; pixmapData=%s" % repr(pixmaps))
|
||||||
|
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
"""
|
"""
|
||||||
Helper functions which smooth out the differences between python 2 and 3.
|
Helper functions that smooth out the differences between python 2 and 3.
|
||||||
"""
|
"""
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
|
68
tests/test_functions.py
Normal file
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.
|
Button displaying a color and allowing the user to select a new color.
|
||||||
|
|
||||||
====================== ============================================================
|
====================== ============================================================
|
||||||
**Signals**:
|
**Signals:**
|
||||||
sigColorChanging(self) emitted whenever a new color is picked in the color dialog
|
sigColorChanging(self) emitted whenever a new color is picked in the color dialog
|
||||||
sigColorChanged(self) emitted when the selected color is accepted (user clicks OK)
|
sigColorChanged(self) emitted when the selected color is accepted (user clicks OK)
|
||||||
====================== ============================================================
|
====================== ============================================================
|
||||||
|
@ -86,14 +86,14 @@ class ColorMapParameter(ptree.types.GroupParameter):
|
|||||||
"""
|
"""
|
||||||
Return an array of colors corresponding to *data*.
|
Return an array of colors corresponding to *data*.
|
||||||
|
|
||||||
========= =================================================================
|
============== =================================================================
|
||||||
Arguments
|
**Arguments:**
|
||||||
data A numpy record array where the fields in data.dtype match those
|
data A numpy record array where the fields in data.dtype match those
|
||||||
defined by a prior call to setFields().
|
defined by a prior call to setFields().
|
||||||
mode Either 'byte' or 'float'. For 'byte', the method returns an array
|
mode Either 'byte' or 'float'. For 'byte', the method returns an array
|
||||||
of dtype ubyte with values scaled 0-255. For 'float', colors are
|
of dtype ubyte with values scaled 0-255. For 'float', colors are
|
||||||
returned as 0.0-1.0 float values.
|
returned as 0.0-1.0 float values.
|
||||||
========= =================================================================
|
============== =================================================================
|
||||||
"""
|
"""
|
||||||
colors = np.zeros((len(data),4))
|
colors = np.zeros((len(data),4))
|
||||||
for item in self.children():
|
for item in self.children():
|
||||||
|
@ -4,9 +4,27 @@ from .GraphicsView import GraphicsView
|
|||||||
|
|
||||||
__all__ = ['GraphicsLayoutWidget']
|
__all__ = ['GraphicsLayoutWidget']
|
||||||
class GraphicsLayoutWidget(GraphicsView):
|
class GraphicsLayoutWidget(GraphicsView):
|
||||||
|
"""
|
||||||
|
Convenience class consisting of a :class:`GraphicsView
|
||||||
|
<pyqtgraph.GraphicsView>` with a single :class:`GraphicsLayout
|
||||||
|
<pyqtgraph.GraphicsLayout>` as its central item.
|
||||||
|
|
||||||
|
This class wraps several methods from its internal GraphicsLayout:
|
||||||
|
:func:`nextRow <pyqtgraph.GraphicsLayout.nextRow>`
|
||||||
|
:func:`nextColumn <pyqtgraph.GraphicsLayout.nextColumn>`
|
||||||
|
:func:`addPlot <pyqtgraph.GraphicsLayout.addPlot>`
|
||||||
|
:func:`addViewBox <pyqtgraph.GraphicsLayout.addViewBox>`
|
||||||
|
:func:`addItem <pyqtgraph.GraphicsLayout.addItem>`
|
||||||
|
:func:`getItem <pyqtgraph.GraphicsLayout.getItem>`
|
||||||
|
:func:`addLabel <pyqtgraph.GraphicsLayout.addLabel>`
|
||||||
|
:func:`addLayout <pyqtgraph.GraphicsLayout.addLayout>`
|
||||||
|
:func:`removeItem <pyqtgraph.GraphicsLayout.removeItem>`
|
||||||
|
:func:`itemIndex <pyqtgraph.GraphicsLayout.itemIndex>`
|
||||||
|
:func:`clear <pyqtgraph.GraphicsLayout.clear>`
|
||||||
|
"""
|
||||||
def __init__(self, parent=None, **kargs):
|
def __init__(self, parent=None, **kargs):
|
||||||
GraphicsView.__init__(self, parent)
|
GraphicsView.__init__(self, parent)
|
||||||
self.ci = GraphicsLayout(**kargs)
|
self.ci = GraphicsLayout(**kargs)
|
||||||
for n in ['nextRow', 'nextCol', 'nextColumn', 'addPlot', 'addViewBox', 'addItem', 'getItem', 'addLabel', 'addLayout', 'addLabel', 'addViewBox', 'removeItem', 'itemIndex', 'clear']:
|
for n in ['nextRow', 'nextCol', 'nextColumn', 'addPlot', 'addViewBox', 'addItem', 'getItem', 'addLayout', 'addLabel', 'removeItem', 'itemIndex', 'clear']:
|
||||||
setattr(self, n, getattr(self.ci, n))
|
setattr(self, n, getattr(self.ci, n))
|
||||||
self.setCentralItem(self.ci)
|
self.setCentralItem(self.ci)
|
||||||
|
@ -40,8 +40,8 @@ class GraphicsView(QtGui.QGraphicsView):
|
|||||||
The view can be panned using the middle mouse button and scaled using the right mouse button if
|
The view can be panned using the middle mouse button and scaled using the right mouse button if
|
||||||
enabled via enableMouse() (but ordinarily, we use ViewBox for this functionality)."""
|
enabled via enableMouse() (but ordinarily, we use ViewBox for this functionality)."""
|
||||||
|
|
||||||
sigRangeChanged = QtCore.Signal(object, object)
|
sigDeviceRangeChanged = QtCore.Signal(object, object)
|
||||||
sigTransformChanged = QtCore.Signal(object)
|
sigDeviceTransformChanged = QtCore.Signal(object)
|
||||||
sigMouseReleased = QtCore.Signal(object)
|
sigMouseReleased = QtCore.Signal(object)
|
||||||
sigSceneMouseMoved = QtCore.Signal(object)
|
sigSceneMouseMoved = QtCore.Signal(object)
|
||||||
#sigRegionChanged = QtCore.Signal(object)
|
#sigRegionChanged = QtCore.Signal(object)
|
||||||
@ -50,21 +50,21 @@ class GraphicsView(QtGui.QGraphicsView):
|
|||||||
|
|
||||||
def __init__(self, parent=None, useOpenGL=None, background='default'):
|
def __init__(self, parent=None, useOpenGL=None, background='default'):
|
||||||
"""
|
"""
|
||||||
============ ============================================================
|
============== ============================================================
|
||||||
Arguments:
|
**Arguments:**
|
||||||
parent Optional parent widget
|
parent Optional parent widget
|
||||||
useOpenGL If True, the GraphicsView will use OpenGL to do all of its
|
useOpenGL If True, the GraphicsView will use OpenGL to do all of its
|
||||||
rendering. This can improve performance on some systems,
|
rendering. This can improve performance on some systems,
|
||||||
but may also introduce bugs (the combination of
|
but may also introduce bugs (the combination of
|
||||||
QGraphicsView and QGLWidget is still an 'experimental'
|
QGraphicsView and QGLWidget is still an 'experimental'
|
||||||
feature of Qt)
|
feature of Qt)
|
||||||
background Set the background color of the GraphicsView. Accepts any
|
background Set the background color of the GraphicsView. Accepts any
|
||||||
single argument accepted by
|
single argument accepted by
|
||||||
:func:`mkColor <pyqtgraph.mkColor>`. By
|
:func:`mkColor <pyqtgraph.mkColor>`. By
|
||||||
default, the background color is determined using the
|
default, the background color is determined using the
|
||||||
'backgroundColor' configuration option (see
|
'backgroundColor' configuration option (see
|
||||||
:func:`setConfigOption <pyqtgraph.setConfigOption>`.
|
:func:`setConfigOption <pyqtgraph.setConfigOption>`.
|
||||||
============ ============================================================
|
============== ============================================================
|
||||||
"""
|
"""
|
||||||
|
|
||||||
self.closed = False
|
self.closed = False
|
||||||
@ -219,8 +219,8 @@ class GraphicsView(QtGui.QGraphicsView):
|
|||||||
else:
|
else:
|
||||||
self.fitInView(self.range, QtCore.Qt.IgnoreAspectRatio)
|
self.fitInView(self.range, QtCore.Qt.IgnoreAspectRatio)
|
||||||
|
|
||||||
self.sigRangeChanged.emit(self, self.range)
|
self.sigDeviceRangeChanged.emit(self, self.range)
|
||||||
self.sigTransformChanged.emit(self)
|
self.sigDeviceTransformChanged.emit(self)
|
||||||
|
|
||||||
if propagate:
|
if propagate:
|
||||||
for v in self.lockedViewports:
|
for v in self.lockedViewports:
|
||||||
@ -287,7 +287,7 @@ class GraphicsView(QtGui.QGraphicsView):
|
|||||||
image.setPxMode(True)
|
image.setPxMode(True)
|
||||||
try:
|
try:
|
||||||
self.sigScaleChanged.disconnect(image.setScaledMode)
|
self.sigScaleChanged.disconnect(image.setScaledMode)
|
||||||
except TypeError:
|
except (TypeError, RuntimeError):
|
||||||
pass
|
pass
|
||||||
tl = image.sceneBoundingRect().topLeft()
|
tl = image.sceneBoundingRect().topLeft()
|
||||||
w = self.size().width() * pxSize[0]
|
w = self.size().width() * pxSize[0]
|
||||||
@ -368,14 +368,14 @@ class GraphicsView(QtGui.QGraphicsView):
|
|||||||
delta = Point(np.clip(delta[0], -50, 50), np.clip(-delta[1], -50, 50))
|
delta = Point(np.clip(delta[0], -50, 50), np.clip(-delta[1], -50, 50))
|
||||||
scale = 1.01 ** delta
|
scale = 1.01 ** delta
|
||||||
self.scale(scale[0], scale[1], center=self.mapToScene(self.mousePressPos))
|
self.scale(scale[0], scale[1], center=self.mapToScene(self.mousePressPos))
|
||||||
self.sigRangeChanged.emit(self, self.range)
|
self.sigDeviceRangeChanged.emit(self, self.range)
|
||||||
|
|
||||||
elif ev.buttons() in [QtCore.Qt.MidButton, QtCore.Qt.LeftButton]: ## Allow panning by left or mid button.
|
elif ev.buttons() in [QtCore.Qt.MidButton, QtCore.Qt.LeftButton]: ## Allow panning by left or mid button.
|
||||||
px = self.pixelSize()
|
px = self.pixelSize()
|
||||||
tr = -delta * px
|
tr = -delta * px
|
||||||
|
|
||||||
self.translate(tr[0], tr[1])
|
self.translate(tr[0], tr[1])
|
||||||
self.sigRangeChanged.emit(self, self.range)
|
self.sigDeviceRangeChanged.emit(self, self.range)
|
||||||
|
|
||||||
def pixelSize(self):
|
def pixelSize(self):
|
||||||
"""Return vector with the length and width of one view pixel in scene coordinates"""
|
"""Return vector with the length and width of one view pixel in scene coordinates"""
|
||||||
|
@ -4,28 +4,43 @@ MultiPlotWidget.py - Convenience class--GraphicsView widget displaying a MultiP
|
|||||||
Copyright 2010 Luke Campagnola
|
Copyright 2010 Luke Campagnola
|
||||||
Distributed under MIT/X11 license. See license.txt for more infomation.
|
Distributed under MIT/X11 license. See license.txt for more infomation.
|
||||||
"""
|
"""
|
||||||
|
from ..Qt import QtCore
|
||||||
from .GraphicsView import GraphicsView
|
from .GraphicsView import GraphicsView
|
||||||
from ..graphicsItems import MultiPlotItem as MultiPlotItem
|
from ..graphicsItems import MultiPlotItem as MultiPlotItem
|
||||||
|
|
||||||
__all__ = ['MultiPlotWidget']
|
__all__ = ['MultiPlotWidget']
|
||||||
class MultiPlotWidget(GraphicsView):
|
class MultiPlotWidget(GraphicsView):
|
||||||
"""Widget implementing a graphicsView with a single PlotItem inside."""
|
"""Widget implementing a graphicsView with a single MultiPlotItem inside."""
|
||||||
def __init__(self, parent=None):
|
def __init__(self, parent=None):
|
||||||
|
self.minPlotHeight = 50
|
||||||
|
self.mPlotItem = MultiPlotItem.MultiPlotItem()
|
||||||
GraphicsView.__init__(self, parent)
|
GraphicsView.__init__(self, parent)
|
||||||
self.enableMouse(False)
|
self.enableMouse(False)
|
||||||
self.mPlotItem = MultiPlotItem.MultiPlotItem()
|
|
||||||
self.setCentralItem(self.mPlotItem)
|
self.setCentralItem(self.mPlotItem)
|
||||||
## Explicitly wrap methods from mPlotItem
|
## Explicitly wrap methods from mPlotItem
|
||||||
#for m in ['setData']:
|
#for m in ['setData']:
|
||||||
#setattr(self, m, getattr(self.mPlotItem, m))
|
#setattr(self, m, getattr(self.mPlotItem, m))
|
||||||
|
self.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAsNeeded)
|
||||||
|
self.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAsNeeded)
|
||||||
|
|
||||||
def __getattr__(self, attr): ## implicitly wrap methods from plotItem
|
def __getattr__(self, attr): ## implicitly wrap methods from plotItem
|
||||||
if hasattr(self.mPlotItem, attr):
|
if hasattr(self.mPlotItem, attr):
|
||||||
m = getattr(self.mPlotItem, attr)
|
m = getattr(self.mPlotItem, attr)
|
||||||
if hasattr(m, '__call__'):
|
if hasattr(m, '__call__'):
|
||||||
return m
|
return m
|
||||||
raise NameError(attr)
|
raise AttributeError(attr)
|
||||||
|
|
||||||
|
def setMinimumPlotHeight(self, min):
|
||||||
|
"""Set the minimum height for each sub-plot displayed.
|
||||||
|
|
||||||
|
If the total height of all plots is greater than the height of the
|
||||||
|
widget, then a scroll bar will appear to provide access to the entire
|
||||||
|
set of plots.
|
||||||
|
|
||||||
|
Added in version 0.9.9
|
||||||
|
"""
|
||||||
|
self.minPlotHeight = min
|
||||||
|
self.resizeEvent(None)
|
||||||
|
|
||||||
def widgetGroupInterface(self):
|
def widgetGroupInterface(self):
|
||||||
return (None, MultiPlotWidget.saveState, MultiPlotWidget.restoreState)
|
return (None, MultiPlotWidget.saveState, MultiPlotWidget.restoreState)
|
||||||
@ -43,3 +58,21 @@ class MultiPlotWidget(GraphicsView):
|
|||||||
self.mPlotItem = None
|
self.mPlotItem = None
|
||||||
self.setParent(None)
|
self.setParent(None)
|
||||||
GraphicsView.close(self)
|
GraphicsView.close(self)
|
||||||
|
|
||||||
|
def setRange(self, *args, **kwds):
|
||||||
|
GraphicsView.setRange(self, *args, **kwds)
|
||||||
|
if self.centralWidget is not None:
|
||||||
|
r = self.range
|
||||||
|
minHeight = len(self.mPlotItem.plots) * self.minPlotHeight
|
||||||
|
if r.height() < minHeight:
|
||||||
|
r.setHeight(minHeight)
|
||||||
|
r.setWidth(r.width() - self.verticalScrollBar().width())
|
||||||
|
self.centralWidget.setGeometry(r)
|
||||||
|
|
||||||
|
def resizeEvent(self, ev):
|
||||||
|
if self.closed:
|
||||||
|
return
|
||||||
|
if self.autoPixelRange:
|
||||||
|
self.range = QtCore.QRectF(0, 0, self.size().width(), self.size().height())
|
||||||
|
MultiPlotWidget.setRange(self, self.range, padding=0, disableAutoPixel=False) ## we do this because some subclasses like to redefine setRange in an incompatible way.
|
||||||
|
self.updateMatrix()
|
||||||
|
@ -23,8 +23,8 @@ class PathButton(QtGui.QPushButton):
|
|||||||
def setBrush(self, brush):
|
def setBrush(self, brush):
|
||||||
self.brush = fn.mkBrush(brush)
|
self.brush = fn.mkBrush(brush)
|
||||||
|
|
||||||
def setPen(self, pen):
|
def setPen(self, *args, **kwargs):
|
||||||
self.pen = fn.mkPen(pen)
|
self.pen = fn.mkPen(*args, **kwargs)
|
||||||
|
|
||||||
def setPath(self, path):
|
def setPath(self, path):
|
||||||
self.path = path
|
self.path = path
|
||||||
@ -46,6 +46,5 @@ class PathButton(QtGui.QPushButton):
|
|||||||
p.setBrush(self.brush)
|
p.setBrush(self.brush)
|
||||||
p.drawPath(self.path)
|
p.drawPath(self.path)
|
||||||
p.end()
|
p.end()
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -12,7 +12,9 @@ from ..graphicsItems.PlotItem import *
|
|||||||
__all__ = ['PlotWidget']
|
__all__ = ['PlotWidget']
|
||||||
class PlotWidget(GraphicsView):
|
class PlotWidget(GraphicsView):
|
||||||
|
|
||||||
#sigRangeChanged = QtCore.Signal(object, object) ## already defined in GraphicsView
|
# signals wrapped from PlotItem / ViewBox
|
||||||
|
sigRangeChanged = QtCore.Signal(object, object)
|
||||||
|
sigTransformChanged = QtCore.Signal(object)
|
||||||
|
|
||||||
"""
|
"""
|
||||||
:class:`GraphicsView <pyqtgraph.GraphicsView>` widget with a single
|
:class:`GraphicsView <pyqtgraph.GraphicsView>` widget with a single
|
||||||
@ -33,6 +35,7 @@ class PlotWidget(GraphicsView):
|
|||||||
:func:`enableAutoRange <pyqtgraph.ViewBox.enableAutoRange>`,
|
:func:`enableAutoRange <pyqtgraph.ViewBox.enableAutoRange>`,
|
||||||
:func:`disableAutoRange <pyqtgraph.ViewBox.disableAutoRange>`,
|
:func:`disableAutoRange <pyqtgraph.ViewBox.disableAutoRange>`,
|
||||||
:func:`setAspectLocked <pyqtgraph.ViewBox.setAspectLocked>`,
|
:func:`setAspectLocked <pyqtgraph.ViewBox.setAspectLocked>`,
|
||||||
|
:func:`setLimits <pyqtgraph.ViewBox.setLimits>`,
|
||||||
:func:`register <pyqtgraph.ViewBox.register>`,
|
:func:`register <pyqtgraph.ViewBox.register>`,
|
||||||
:func:`unregister <pyqtgraph.ViewBox.unregister>`
|
:func:`unregister <pyqtgraph.ViewBox.unregister>`
|
||||||
|
|
||||||
@ -52,7 +55,10 @@ class PlotWidget(GraphicsView):
|
|||||||
self.setCentralItem(self.plotItem)
|
self.setCentralItem(self.plotItem)
|
||||||
## Explicitly wrap methods from plotItem
|
## Explicitly wrap methods from plotItem
|
||||||
## NOTE: If you change this list, update the documentation above as well.
|
## NOTE: If you change this list, update the documentation above as well.
|
||||||
for m in ['addItem', 'removeItem', 'autoRange', 'clear', 'setXRange', 'setYRange', 'setRange', 'setAspectLocked', 'setMouseEnabled', 'setXLink', 'setYLink', 'enableAutoRange', 'disableAutoRange', 'register', 'unregister', 'viewRect']:
|
for m in ['addItem', 'removeItem', 'autoRange', 'clear', 'setXRange',
|
||||||
|
'setYRange', 'setRange', 'setAspectLocked', 'setMouseEnabled',
|
||||||
|
'setXLink', 'setYLink', 'enableAutoRange', 'disableAutoRange',
|
||||||
|
'setLimits', 'register', 'unregister', 'viewRect']:
|
||||||
setattr(self, m, getattr(self.plotItem, m))
|
setattr(self, m, getattr(self.plotItem, m))
|
||||||
#QtCore.QObject.connect(self.plotItem, QtCore.SIGNAL('viewChanged'), self.viewChanged)
|
#QtCore.QObject.connect(self.plotItem, QtCore.SIGNAL('viewChanged'), self.viewChanged)
|
||||||
self.plotItem.sigRangeChanged.connect(self.viewRangeChanged)
|
self.plotItem.sigRangeChanged.connect(self.viewRangeChanged)
|
||||||
|
@ -3,6 +3,7 @@ if not USE_PYSIDE:
|
|||||||
import sip
|
import sip
|
||||||
from .. import multiprocess as mp
|
from .. import multiprocess as mp
|
||||||
from .GraphicsView import GraphicsView
|
from .GraphicsView import GraphicsView
|
||||||
|
from .. import CONFIG_OPTIONS
|
||||||
import numpy as np
|
import numpy as np
|
||||||
import mmap, tempfile, ctypes, atexit, sys, random
|
import mmap, tempfile, ctypes, atexit, sys, random
|
||||||
|
|
||||||
@ -35,7 +36,7 @@ class RemoteGraphicsView(QtGui.QWidget):
|
|||||||
|
|
||||||
self._proc = mp.QtProcess(**kwds)
|
self._proc = mp.QtProcess(**kwds)
|
||||||
self.pg = self._proc._import('pyqtgraph')
|
self.pg = self._proc._import('pyqtgraph')
|
||||||
self.pg.setConfigOptions(**self.pg.CONFIG_OPTIONS)
|
self.pg.setConfigOptions(**CONFIG_OPTIONS)
|
||||||
rpgRemote = self._proc._import('pyqtgraph.widgets.RemoteGraphicsView')
|
rpgRemote = self._proc._import('pyqtgraph.widgets.RemoteGraphicsView')
|
||||||
self._view = rpgRemote.Renderer(*args, **remoteKwds)
|
self._view = rpgRemote.Renderer(*args, **remoteKwds)
|
||||||
self._view._setProxyOptions(deferGetattr=True)
|
self._view._setProxyOptions(deferGetattr=True)
|
||||||
|
@ -9,7 +9,28 @@ try:
|
|||||||
except ImportError:
|
except ImportError:
|
||||||
HAVE_METAARRAY = False
|
HAVE_METAARRAY = False
|
||||||
|
|
||||||
|
|
||||||
__all__ = ['TableWidget']
|
__all__ = ['TableWidget']
|
||||||
|
|
||||||
|
|
||||||
|
def _defersort(fn):
|
||||||
|
def defersort(self, *args, **kwds):
|
||||||
|
# may be called recursively; only the first call needs to block sorting
|
||||||
|
setSorting = False
|
||||||
|
if self._sorting is None:
|
||||||
|
self._sorting = self.isSortingEnabled()
|
||||||
|
setSorting = True
|
||||||
|
self.setSortingEnabled(False)
|
||||||
|
try:
|
||||||
|
return fn(self, *args, **kwds)
|
||||||
|
finally:
|
||||||
|
if setSorting:
|
||||||
|
self.setSortingEnabled(self._sorting)
|
||||||
|
self._sorting = None
|
||||||
|
|
||||||
|
return defersort
|
||||||
|
|
||||||
|
|
||||||
class TableWidget(QtGui.QTableWidget):
|
class TableWidget(QtGui.QTableWidget):
|
||||||
"""Extends QTableWidget with some useful functions for automatic data handling
|
"""Extends QTableWidget with some useful functions for automatic data handling
|
||||||
and copy / export context menu. Can automatically format and display a variety
|
and copy / export context menu. Can automatically format and display a variety
|
||||||
@ -18,14 +39,45 @@ class TableWidget(QtGui.QTableWidget):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, *args, **kwds):
|
def __init__(self, *args, **kwds):
|
||||||
|
"""
|
||||||
|
All positional arguments are passed to QTableWidget.__init__().
|
||||||
|
|
||||||
|
===================== =================================================
|
||||||
|
**Keyword Arguments**
|
||||||
|
editable (bool) If True, cells in the table can be edited
|
||||||
|
by the user. Default is False.
|
||||||
|
sortable (bool) If True, the table may be soted by
|
||||||
|
clicking on column headers. Note that this also
|
||||||
|
causes rows to appear initially shuffled until
|
||||||
|
a sort column is selected. Default is True.
|
||||||
|
*(added in version 0.9.9)*
|
||||||
|
===================== =================================================
|
||||||
|
"""
|
||||||
|
|
||||||
QtGui.QTableWidget.__init__(self, *args)
|
QtGui.QTableWidget.__init__(self, *args)
|
||||||
|
|
||||||
|
self.itemClass = TableWidgetItem
|
||||||
|
|
||||||
self.setVerticalScrollMode(self.ScrollPerPixel)
|
self.setVerticalScrollMode(self.ScrollPerPixel)
|
||||||
self.setSelectionMode(QtGui.QAbstractItemView.ContiguousSelection)
|
self.setSelectionMode(QtGui.QAbstractItemView.ContiguousSelection)
|
||||||
self.setSizePolicy(QtGui.QSizePolicy.Preferred, QtGui.QSizePolicy.Preferred)
|
self.setSizePolicy(QtGui.QSizePolicy.Preferred, QtGui.QSizePolicy.Preferred)
|
||||||
self.setSortingEnabled(True)
|
|
||||||
self.clear()
|
self.clear()
|
||||||
editable = kwds.get('editable', False)
|
|
||||||
self.setEditable(editable)
|
kwds.setdefault('sortable', True)
|
||||||
|
kwds.setdefault('editable', False)
|
||||||
|
self.setEditable(kwds.pop('editable'))
|
||||||
|
self.setSortingEnabled(kwds.pop('sortable'))
|
||||||
|
|
||||||
|
if len(kwds) > 0:
|
||||||
|
raise TypeError("Invalid keyword arguments '%s'" % kwds.keys())
|
||||||
|
|
||||||
|
self._sorting = None # used when temporarily disabling sorting
|
||||||
|
|
||||||
|
self._formats = {None: None} # stores per-column formats and entire table format
|
||||||
|
self.sortModes = {} # stores per-column sort mode
|
||||||
|
|
||||||
|
self.itemChanged.connect(self.handleItemChanged)
|
||||||
|
|
||||||
self.contextMenu = QtGui.QMenu()
|
self.contextMenu = QtGui.QMenu()
|
||||||
self.contextMenu.addAction('Copy Selection').triggered.connect(self.copySel)
|
self.contextMenu.addAction('Copy Selection').triggered.connect(self.copySel)
|
||||||
self.contextMenu.addAction('Copy All').triggered.connect(self.copyAll)
|
self.contextMenu.addAction('Copy All').triggered.connect(self.copyAll)
|
||||||
@ -40,6 +92,7 @@ class TableWidget(QtGui.QTableWidget):
|
|||||||
self.items = []
|
self.items = []
|
||||||
self.setRowCount(0)
|
self.setRowCount(0)
|
||||||
self.setColumnCount(0)
|
self.setColumnCount(0)
|
||||||
|
self.sortModes = {}
|
||||||
|
|
||||||
def setData(self, data):
|
def setData(self, data):
|
||||||
"""Set the data displayed in the table.
|
"""Set the data displayed in the table.
|
||||||
@ -56,12 +109,16 @@ class TableWidget(QtGui.QTableWidget):
|
|||||||
self.appendData(data)
|
self.appendData(data)
|
||||||
self.resizeColumnsToContents()
|
self.resizeColumnsToContents()
|
||||||
|
|
||||||
|
@_defersort
|
||||||
def appendData(self, data):
|
def appendData(self, data):
|
||||||
"""Types allowed:
|
|
||||||
1 or 2D numpy array or metaArray
|
|
||||||
1D numpy record array
|
|
||||||
list-of-lists, list-of-dicts or dict-of-lists
|
|
||||||
"""
|
"""
|
||||||
|
Add new rows to the table.
|
||||||
|
|
||||||
|
See :func:`setData() <pyqtgraph.TableWidget.setData>` for accepted
|
||||||
|
data types.
|
||||||
|
"""
|
||||||
|
startRow = self.rowCount()
|
||||||
|
|
||||||
fn0, header0 = self.iteratorFn(data)
|
fn0, header0 = self.iteratorFn(data)
|
||||||
if fn0 is None:
|
if fn0 is None:
|
||||||
self.clear()
|
self.clear()
|
||||||
@ -80,42 +137,88 @@ class TableWidget(QtGui.QTableWidget):
|
|||||||
self.setColumnCount(len(firstVals))
|
self.setColumnCount(len(firstVals))
|
||||||
|
|
||||||
if not self.verticalHeadersSet and header0 is not None:
|
if not self.verticalHeadersSet and header0 is not None:
|
||||||
self.setRowCount(len(header0))
|
labels = [self.verticalHeaderItem(i).text() for i in range(self.rowCount())]
|
||||||
self.setVerticalHeaderLabels(header0)
|
self.setRowCount(startRow + len(header0))
|
||||||
|
self.setVerticalHeaderLabels(labels + header0)
|
||||||
self.verticalHeadersSet = True
|
self.verticalHeadersSet = True
|
||||||
if not self.horizontalHeadersSet and header1 is not None:
|
if not self.horizontalHeadersSet and header1 is not None:
|
||||||
self.setHorizontalHeaderLabels(header1)
|
self.setHorizontalHeaderLabels(header1)
|
||||||
self.horizontalHeadersSet = True
|
self.horizontalHeadersSet = True
|
||||||
|
|
||||||
self.setRow(0, firstVals)
|
i = startRow
|
||||||
i = 1
|
self.setRow(i, firstVals)
|
||||||
for row in it0:
|
for row in it0:
|
||||||
self.setRow(i, [x for x in fn1(row)])
|
|
||||||
i += 1
|
i += 1
|
||||||
|
self.setRow(i, [x for x in fn1(row)])
|
||||||
|
|
||||||
|
if self._sorting and self.horizontalHeader().sortIndicatorSection() >= self.columnCount():
|
||||||
|
self.sortByColumn(0, QtCore.Qt.AscendingOrder)
|
||||||
|
|
||||||
def setEditable(self, editable=True):
|
def setEditable(self, editable=True):
|
||||||
self.editable = editable
|
self.editable = editable
|
||||||
for item in self.items:
|
for item in self.items:
|
||||||
item.setEditable(editable)
|
item.setEditable(editable)
|
||||||
|
|
||||||
|
def setFormat(self, format, column=None):
|
||||||
|
"""
|
||||||
|
Specify the default text formatting for the entire table, or for a
|
||||||
|
single column if *column* is specified.
|
||||||
|
|
||||||
|
If a string is specified, it is used as a format string for converting
|
||||||
|
float values (and all other types are converted using str). If a
|
||||||
|
function is specified, it will be called with the item as its only
|
||||||
|
argument and must return a string. Setting format = None causes the
|
||||||
|
default formatter to be used instead.
|
||||||
|
|
||||||
|
Added in version 0.9.9.
|
||||||
|
|
||||||
|
"""
|
||||||
|
if format is not None and not isinstance(format, basestring) and not callable(format):
|
||||||
|
raise ValueError("Format argument must string, callable, or None. (got %s)" % format)
|
||||||
|
|
||||||
|
self._formats[column] = format
|
||||||
|
|
||||||
|
|
||||||
|
if column is None:
|
||||||
|
# update format of all items that do not have a column format
|
||||||
|
# specified
|
||||||
|
for c in range(self.columnCount()):
|
||||||
|
if self._formats.get(c, None) is None:
|
||||||
|
for r in range(self.rowCount()):
|
||||||
|
item = self.item(r, c)
|
||||||
|
if item is None:
|
||||||
|
continue
|
||||||
|
item.setFormat(format)
|
||||||
|
else:
|
||||||
|
# set all items in the column to use this format, or the default
|
||||||
|
# table format if None was specified.
|
||||||
|
if format is None:
|
||||||
|
format = self._formats[None]
|
||||||
|
for r in range(self.rowCount()):
|
||||||
|
item = self.item(r, column)
|
||||||
|
if item is None:
|
||||||
|
continue
|
||||||
|
item.setFormat(format)
|
||||||
|
|
||||||
|
|
||||||
def iteratorFn(self, data):
|
def iteratorFn(self, data):
|
||||||
## Return 1) a function that will provide an iterator for data and 2) a list of header strings
|
## Return 1) a function that will provide an iterator for data and 2) a list of header strings
|
||||||
if isinstance(data, list) or isinstance(data, tuple):
|
if isinstance(data, list) or isinstance(data, tuple):
|
||||||
return lambda d: d.__iter__(), None
|
return lambda d: d.__iter__(), None
|
||||||
elif isinstance(data, dict):
|
elif isinstance(data, dict):
|
||||||
return lambda d: iter(d.values()), list(map(str, data.keys()))
|
return lambda d: iter(d.values()), list(map(asUnicode, data.keys()))
|
||||||
elif HAVE_METAARRAY and (hasattr(data, 'implements') and data.implements('MetaArray')):
|
elif HAVE_METAARRAY and (hasattr(data, 'implements') and data.implements('MetaArray')):
|
||||||
if data.axisHasColumns(0):
|
if data.axisHasColumns(0):
|
||||||
header = [str(data.columnName(0, i)) for i in range(data.shape[0])]
|
header = [asUnicode(data.columnName(0, i)) for i in range(data.shape[0])]
|
||||||
elif data.axisHasValues(0):
|
elif data.axisHasValues(0):
|
||||||
header = list(map(str, data.xvals(0)))
|
header = list(map(asUnicode, data.xvals(0)))
|
||||||
else:
|
else:
|
||||||
header = None
|
header = None
|
||||||
return self.iterFirstAxis, header
|
return self.iterFirstAxis, header
|
||||||
elif isinstance(data, np.ndarray):
|
elif isinstance(data, np.ndarray):
|
||||||
return self.iterFirstAxis, None
|
return self.iterFirstAxis, None
|
||||||
elif isinstance(data, np.void):
|
elif isinstance(data, np.void):
|
||||||
return self.iterate, list(map(str, data.dtype.names))
|
return self.iterate, list(map(asUnicode, data.dtype.names))
|
||||||
elif data is None:
|
elif data is None:
|
||||||
return (None,None)
|
return (None,None)
|
||||||
else:
|
else:
|
||||||
@ -135,21 +238,50 @@ class TableWidget(QtGui.QTableWidget):
|
|||||||
def appendRow(self, data):
|
def appendRow(self, data):
|
||||||
self.appendData([data])
|
self.appendData([data])
|
||||||
|
|
||||||
|
@_defersort
|
||||||
def addRow(self, vals):
|
def addRow(self, vals):
|
||||||
row = self.rowCount()
|
row = self.rowCount()
|
||||||
self.setRowCount(row + 1)
|
self.setRowCount(row + 1)
|
||||||
self.setRow(row, vals)
|
self.setRow(row, vals)
|
||||||
|
|
||||||
|
@_defersort
|
||||||
def setRow(self, row, vals):
|
def setRow(self, row, vals):
|
||||||
if row > self.rowCount() - 1:
|
if row > self.rowCount() - 1:
|
||||||
self.setRowCount(row + 1)
|
self.setRowCount(row + 1)
|
||||||
for col in range(len(vals)):
|
for col in range(len(vals)):
|
||||||
val = vals[col]
|
val = vals[col]
|
||||||
item = TableWidgetItem(val)
|
item = self.itemClass(val, row)
|
||||||
item.setEditable(self.editable)
|
item.setEditable(self.editable)
|
||||||
|
sortMode = self.sortModes.get(col, None)
|
||||||
|
if sortMode is not None:
|
||||||
|
item.setSortMode(sortMode)
|
||||||
|
format = self._formats.get(col, self._formats[None])
|
||||||
|
item.setFormat(format)
|
||||||
self.items.append(item)
|
self.items.append(item)
|
||||||
self.setItem(row, col, item)
|
self.setItem(row, col, item)
|
||||||
|
item.setValue(val) # Required--the text-change callback is invoked
|
||||||
|
# when we call setItem.
|
||||||
|
|
||||||
|
def setSortMode(self, column, mode):
|
||||||
|
"""
|
||||||
|
Set the mode used to sort *column*.
|
||||||
|
|
||||||
|
============== ========================================================
|
||||||
|
**Sort Modes**
|
||||||
|
value Compares item.value if available; falls back to text
|
||||||
|
comparison.
|
||||||
|
text Compares item.text()
|
||||||
|
index Compares by the order in which items were inserted.
|
||||||
|
============== ========================================================
|
||||||
|
|
||||||
|
Added in version 0.9.9
|
||||||
|
"""
|
||||||
|
for r in range(self.rowCount()):
|
||||||
|
item = self.item(r, column)
|
||||||
|
if hasattr(item, 'setSortMode'):
|
||||||
|
item.setSortMode(mode)
|
||||||
|
self.sortModes[column] = mode
|
||||||
|
|
||||||
def sizeHint(self):
|
def sizeHint(self):
|
||||||
# based on http://stackoverflow.com/a/7195443/54056
|
# based on http://stackoverflow.com/a/7195443/54056
|
||||||
width = sum(self.columnWidth(i) for i in range(self.columnCount()))
|
width = sum(self.columnWidth(i) for i in range(self.columnCount()))
|
||||||
@ -173,7 +305,6 @@ class TableWidget(QtGui.QTableWidget):
|
|||||||
rows = list(range(self.rowCount()))
|
rows = list(range(self.rowCount()))
|
||||||
columns = list(range(self.columnCount()))
|
columns = list(range(self.columnCount()))
|
||||||
|
|
||||||
|
|
||||||
data = []
|
data = []
|
||||||
if self.horizontalHeadersSet:
|
if self.horizontalHeadersSet:
|
||||||
row = []
|
row = []
|
||||||
@ -222,7 +353,6 @@ class TableWidget(QtGui.QTableWidget):
|
|||||||
if fileName == '':
|
if fileName == '':
|
||||||
return
|
return
|
||||||
open(fileName, 'w').write(data)
|
open(fileName, 'w').write(data)
|
||||||
|
|
||||||
|
|
||||||
def contextMenuEvent(self, ev):
|
def contextMenuEvent(self, ev):
|
||||||
self.contextMenu.popup(ev.globalPos())
|
self.contextMenu.popup(ev.globalPos())
|
||||||
@ -234,25 +364,102 @@ class TableWidget(QtGui.QTableWidget):
|
|||||||
else:
|
else:
|
||||||
ev.ignore()
|
ev.ignore()
|
||||||
|
|
||||||
|
def handleItemChanged(self, item):
|
||||||
|
item.textChanged()
|
||||||
|
|
||||||
|
|
||||||
class TableWidgetItem(QtGui.QTableWidgetItem):
|
class TableWidgetItem(QtGui.QTableWidgetItem):
|
||||||
def __init__(self, val):
|
def __init__(self, val, index, format=None):
|
||||||
if isinstance(val, float) or isinstance(val, np.floating):
|
QtGui.QTableWidgetItem.__init__(self, '')
|
||||||
s = "%0.3g" % val
|
self._blockValueChange = False
|
||||||
else:
|
self._format = None
|
||||||
s = str(val)
|
self._defaultFormat = '%0.3g'
|
||||||
QtGui.QTableWidgetItem.__init__(self, s)
|
self.sortMode = 'value'
|
||||||
self.value = val
|
self.index = index
|
||||||
flags = QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled
|
flags = QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled
|
||||||
self.setFlags(flags)
|
self.setFlags(flags)
|
||||||
|
self.setValue(val)
|
||||||
|
self.setFormat(format)
|
||||||
|
|
||||||
def setEditable(self, editable):
|
def setEditable(self, editable):
|
||||||
|
"""
|
||||||
|
Set whether this item is user-editable.
|
||||||
|
"""
|
||||||
if editable:
|
if editable:
|
||||||
self.setFlags(self.flags() | QtCore.Qt.ItemIsEditable)
|
self.setFlags(self.flags() | QtCore.Qt.ItemIsEditable)
|
||||||
else:
|
else:
|
||||||
self.setFlags(self.flags() & ~QtCore.Qt.ItemIsEditable)
|
self.setFlags(self.flags() & ~QtCore.Qt.ItemIsEditable)
|
||||||
|
|
||||||
|
def setSortMode(self, mode):
|
||||||
|
"""
|
||||||
|
Set the mode used to sort this item against others in its column.
|
||||||
|
|
||||||
|
============== ========================================================
|
||||||
|
**Sort Modes**
|
||||||
|
value Compares item.value if available; falls back to text
|
||||||
|
comparison.
|
||||||
|
text Compares item.text()
|
||||||
|
index Compares by the order in which items were inserted.
|
||||||
|
============== ========================================================
|
||||||
|
"""
|
||||||
|
modes = ('value', 'text', 'index', None)
|
||||||
|
if mode not in modes:
|
||||||
|
raise ValueError('Sort mode must be one of %s' % str(modes))
|
||||||
|
self.sortMode = mode
|
||||||
|
|
||||||
|
def setFormat(self, fmt):
|
||||||
|
"""Define the conversion from item value to displayed text.
|
||||||
|
|
||||||
|
If a string is specified, it is used as a format string for converting
|
||||||
|
float values (and all other types are converted using str). If a
|
||||||
|
function is specified, it will be called with the item as its only
|
||||||
|
argument and must return a string.
|
||||||
|
|
||||||
|
Added in version 0.9.9.
|
||||||
|
"""
|
||||||
|
if fmt is not None and not isinstance(fmt, basestring) and not callable(fmt):
|
||||||
|
raise ValueError("Format argument must string, callable, or None. (got %s)" % fmt)
|
||||||
|
self._format = fmt
|
||||||
|
self._updateText()
|
||||||
|
|
||||||
|
def _updateText(self):
|
||||||
|
self._blockValueChange = True
|
||||||
|
try:
|
||||||
|
self.setText(self.format())
|
||||||
|
finally:
|
||||||
|
self._blockValueChange = False
|
||||||
|
|
||||||
|
def setValue(self, value):
|
||||||
|
self.value = value
|
||||||
|
self._updateText()
|
||||||
|
|
||||||
|
def textChanged(self):
|
||||||
|
"""Called when this item's text has changed for any reason."""
|
||||||
|
if self._blockValueChange:
|
||||||
|
# text change was result of value or format change; do not
|
||||||
|
# propagate.
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
self.value = type(self.value)(self.text())
|
||||||
|
except ValueError:
|
||||||
|
self.value = str(self.text())
|
||||||
|
|
||||||
|
def format(self):
|
||||||
|
if callable(self._format):
|
||||||
|
return self._format(self)
|
||||||
|
if isinstance(self.value, (float, np.floating)):
|
||||||
|
if self._format is None:
|
||||||
|
return self._defaultFormat % self.value
|
||||||
|
else:
|
||||||
|
return self._format % self.value
|
||||||
|
else:
|
||||||
|
return asUnicode(self.value)
|
||||||
|
|
||||||
def __lt__(self, other):
|
def __lt__(self, other):
|
||||||
if hasattr(other, 'value'):
|
if self.sortMode == 'index' and hasattr(other, 'index'):
|
||||||
|
return self.index < other.index
|
||||||
|
if self.sortMode == 'value' and hasattr(other, 'value'):
|
||||||
return self.value < other.value
|
return self.value < other.value
|
||||||
else:
|
else:
|
||||||
return self.text() < other.text()
|
return self.text() < other.text()
|
||||||
|
@ -16,18 +16,18 @@ class ValueLabel(QtGui.QLabel):
|
|||||||
|
|
||||||
def __init__(self, parent=None, suffix='', siPrefix=False, averageTime=0, formatStr=None):
|
def __init__(self, parent=None, suffix='', siPrefix=False, averageTime=0, formatStr=None):
|
||||||
"""
|
"""
|
||||||
============ ==================================================================================
|
============== ==================================================================================
|
||||||
Arguments
|
**Arguments:**
|
||||||
suffix (str or None) The suffix to place after the value
|
suffix (str or None) The suffix to place after the value
|
||||||
siPrefix (bool) Whether to add an SI prefix to the units and display a scaled value
|
siPrefix (bool) Whether to add an SI prefix to the units and display a scaled value
|
||||||
averageTime (float) The length of time in seconds to average values. If this value
|
averageTime (float) The length of time in seconds to average values. If this value
|
||||||
is 0, then no averaging is performed. As this value increases
|
is 0, then no averaging is performed. As this value increases
|
||||||
the display value will appear to change more slowly and smoothly.
|
the display value will appear to change more slowly and smoothly.
|
||||||
formatStr (str) Optionally, provide a format string to use when displaying text. The text
|
formatStr (str) Optionally, provide a format string to use when displaying text. The text
|
||||||
will be generated by calling formatStr.format(value=, avgValue=, suffix=)
|
will be generated by calling formatStr.format(value=, avgValue=, suffix=)
|
||||||
(see Python documentation on str.format)
|
(see Python documentation on str.format)
|
||||||
This option is not compatible with siPrefix
|
This option is not compatible with siPrefix
|
||||||
============ ==================================================================================
|
============== ==================================================================================
|
||||||
"""
|
"""
|
||||||
QtGui.QLabel.__init__(self, parent)
|
QtGui.QLabel.__init__(self, parent)
|
||||||
self.values = []
|
self.values = []
|
||||||
|
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