Merge branch 'develop' into core

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

View File

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

View File

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

20
Qt.py
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

123
debug.py
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -30,8 +30,8 @@ class LinearRegionItem(UIGraphicsItem):
def __init__(self, values=[0,1], orientation=None, brush=None, movable=True, bounds=None):
"""Create a new LinearRegionItem.
============= =====================================================================
**Arguments**
============== =====================================================================
**Arguments:**
values A list of the positions of the lines in the region. These are not
limits; limits can be set by specifying bounds.
orientation Options are LinearRegionItem.Vertical or LinearRegionItem.Horizontal.
@ -42,7 +42,7 @@ class LinearRegionItem(UIGraphicsItem):
movable If True, the region and individual lines are movable by the user; if
False, they are static.
bounds Optional [min, max] bounding values for the region
============= =====================================================================
============== =====================================================================
"""
UIGraphicsItem.__init__(self)
@ -89,10 +89,10 @@ class LinearRegionItem(UIGraphicsItem):
def setRegion(self, rgn):
"""Set the values for the edges of the region.
============= ==============================================
**Arguments**
============== ==============================================
**Arguments:**
rgn A list or tuple of the lower and upper values.
============= ==============================================
============== ==============================================
"""
if self.lines[0].value() == rgn[0] and self.lines[1].value() == rgn[1]:
return

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -9,8 +9,8 @@ class TextItem(UIGraphicsItem):
"""
def __init__(self, text='', color=(200,200,200), html=None, anchor=(0,0), border=None, fill=None, angle=0):
"""
=========== =================================================================================
Arguments:
============== =================================================================================
**Arguments:**
*text* The text to display
*color* The color of the text (any format accepted by pg.mkColor)
*html* If specified, this overrides both *text* and *color*
@ -20,7 +20,7 @@ class TextItem(UIGraphicsItem):
sets the lower-right corner.
*border* A pen to use when drawing the border
*fill* A brush to use when filling within the border
=========== =================================================================================
============== =================================================================================
"""
## not working yet

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -7,6 +7,9 @@ except ImportError:
import builtins
import pickle
# color printing for debugging
from ..util import cprint
class ClosedError(Exception):
"""Raised when an event handler receives a request to close the connection
or discovers that the connection has been closed."""
@ -80,7 +83,7 @@ class RemoteEventHandler(object):
def debugMsg(self, msg):
if not self.debug:
return
print("[%d] %s" % (os.getpid(), str(msg)))
cprint.cout(self.debug, "[%d] %s\n" % (os.getpid(), str(msg)), -1)
def getProxyOption(self, opt):
return self.proxyOptions[opt]
@ -299,8 +302,8 @@ class RemoteEventHandler(object):
(The docstring has information that is nevertheless useful to the programmer
as it describes the internal protocol used to communicate between processes)
========== ====================================================================
Arguments:
============== ====================================================================
**Arguments:**
request String describing the type of request being sent (see below)
reqId Integer uniquely linking a result back to the request that generated
it. (most requests leave this blank)
@ -315,7 +318,7 @@ class RemoteEventHandler(object):
byteData If specified, this is a list of objects to be sent as byte messages
to the remote process.
This is used to send large arrays without the cost of pickling.
========== ====================================================================
============== ====================================================================
Description of request strings and options allowed for each:

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -107,8 +107,8 @@ class Parameter(QtCore.QObject):
Parameter instance, the options available to this method are also allowed
by most Parameter subclasses.
================= =========================================================
Keyword Arguments
======================= =========================================================
**Keyword Arguments:**
name The name to give this Parameter. This is the name that
will appear in the left-most column of a ParameterTree
for this Parameter.
@ -133,7 +133,7 @@ class Parameter(QtCore.QObject):
expanded If True, the Parameter will appear expanded when
displayed in a ParameterTree (its children will be
visible). (default=True)
================= =========================================================
======================= =========================================================
"""
@ -516,7 +516,7 @@ class Parameter(QtCore.QObject):
self.sigChildRemoved.emit(self, child)
try:
child.sigTreeStateChanged.disconnect(self.treeStateChanged)
except TypeError: ## already disconnected
except (TypeError, RuntimeError): ## already disconnected
pass
def clearChildren(self):
@ -675,13 +675,13 @@ class Parameter(QtCore.QObject):
"""
Called when the state of any sub-parameter has changed.
========== ================================================================
Arguments:
============== ================================================================
**Arguments:**
param The immediate child whose tree state has changed.
note that the change may have originated from a grandchild.
changes List of tuples describing all changes that have been made
in this event: (param, changeDescr, data)
========== ================================================================
============== ================================================================
This function can be extended to react to tree state changes.
"""

View File

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

View File

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

68
tests/test_functions.py Normal file
View File

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

10
tests/test_qt.py Normal file
View File

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

View File

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

0
util/__init__.py Normal file
View File

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

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

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

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

View File

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

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

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

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

101
util/cprint.py Normal file
View File

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

121
util/lru_cache.py Normal file
View File

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

94
util/mutex.py Normal file
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -16,8 +16,8 @@ class ValueLabel(QtGui.QLabel):
def __init__(self, parent=None, suffix='', siPrefix=False, averageTime=0, formatStr=None):
"""
============ ==================================================================================
Arguments
============== ==================================================================================
**Arguments:**
suffix (str or None) The suffix to place after the value
siPrefix (bool) Whether to add an SI prefix to the units and display a scaled value
averageTime (float) The length of time in seconds to average values. If this value
@ -27,7 +27,7 @@ class ValueLabel(QtGui.QLabel):
will be generated by calling formatStr.format(value=, avgValue=, suffix=)
(see Python documentation on str.format)
This option is not compatible with siPrefix
============ ==================================================================================
============== ==================================================================================
"""
QtGui.QLabel.__init__(self, parent)
self.values = []

View File

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