Merge tag 'pyqtgraph-0.9.8' into pyqtgraph-core

This commit is contained in:
Luke Campagnola 2013-12-23 11:15:54 -05:00
commit 757dc50447
75 changed files with 3293 additions and 1190 deletions

View File

@ -15,6 +15,7 @@ class PlotData(object):
- removal of nan/inf values
- option for single value shared by entire column
- cached downsampling
- cached min / max / hasnan / isuniform
"""
def __init__(self):
self.fields = {}

View File

@ -80,6 +80,12 @@ class Point(QtCore.QPointF):
def __div__(self, a):
return self._math_('__div__', a)
def __truediv__(self, a):
return self._math_('__truediv__', a)
def __rtruediv__(self, a):
return self._math_('__rtruediv__', a)
def __rpow__(self, a):
return self._math_('__rpow__', a)
@ -146,4 +152,4 @@ class Point(QtCore.QPointF):
return Point(self)
def toQPoint(self):
return QtCore.QPoint(*self)
return QtCore.QPoint(*self)

View File

@ -130,11 +130,14 @@ class SRTTransform(QtGui.QTransform):
self._state['angle'] = angle
self.update()
def __div__(self, t):
def __truediv__(self, t):
"""A / B == B^-1 * A"""
dt = t.inverted()[0] * self
return SRTTransform(dt)
def __div__(self, t):
return self.__truediv__(t)
def __mul__(self, t):
return SRTTransform(QtGui.QTransform.__mul__(self, t))

View File

@ -123,7 +123,6 @@ class SRTTransform3D(pg.Transform3D):
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
@ -141,18 +140,30 @@ class SRTTransform3D(pg.Transform3D):
print("Scale: %s" % str(scale))
print("Original matrix: %s" % str(m))
raise
eigIndex = np.argwhere(np.abs(evals-1) < 1e-7)
eigIndex = np.argwhere(np.abs(evals-1) < 1e-6)
if len(eigIndex) < 1:
print("eigenvalues: %s" % str(evals))
print("eigenvectors: %s" % str(evecs))
print("index: %s, %s" % (str(eigIndex), str(evals-1)))
raise Exception("Could not determine rotation axis.")
axis = evecs[eigIndex[0,0]].real
axis = evecs[:,eigIndex[0,0]].real
axis /= ((axis**2).sum())**0.5
self._state['axis'] = axis
## trace(r) == 2 cos(angle) + 1, so:
self._state['angle'] = np.arccos((r.trace()-1)*0.5) * 180 / np.pi
cos = (r.trace()-1)*0.5 ## this only gets us abs(angle)
## The off-diagonal values can be used to correct the angle ambiguity,
## but we need to figure out which element to use:
axisInd = np.argmax(np.abs(axis))
rInd,sign = [((1,2), -1), ((0,2), 1), ((0,1), -1)][axisInd]
## Then we have r-r.T = sin(angle) * 2 * sign * axis[axisInd];
## solve for sin(angle)
sin = (r-r.T)[rInd] / (2. * sign * axis[axisInd])
## finally, we get the complete angle from arctan(sin/cos)
self._state['angle'] = np.arctan2(sin, cos) * 180 / np.pi
if self._state['angle'] == 0:
self._state['axis'] = (0,0,1)

View File

@ -5,7 +5,7 @@ Copyright 2010 Luke Campagnola
Distributed under MIT/X11 license. See license.txt for more infomation.
"""
from .Qt import QtGui, QtCore
from .Qt import QtGui, QtCore, USE_PYSIDE
import numpy as np
class Vector(QtGui.QVector3D):
@ -33,7 +33,13 @@ class Vector(QtGui.QVector3D):
def __len__(self):
return 3
def __add__(self, b):
# workaround for pyside bug. see https://bugs.launchpad.net/pyqtgraph/+bug/1223173
if USE_PYSIDE and isinstance(b, QtGui.QVector3D):
b = Vector(b)
return QtGui.QVector3D.__add__(self, b)
#def __reduce__(self):
#return (Point, (self.x(), self.y()))

View File

@ -54,6 +54,8 @@ CONFIG_OPTIONS = {
'editorCommand': None, ## command used to invoke code editor from ConsoleWidgets
'useWeave': True, ## 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)
}
@ -137,7 +139,7 @@ def importModules(path, globals, locals, excludes=()):
d = os.path.join(os.path.split(globals['__file__'])[0], path)
files = set()
for f in frozenSupport.listdir(d):
if frozenSupport.isdir(os.path.join(d, f)) and f != '__pycache__':
if frozenSupport.isdir(os.path.join(d, f)) and f not in ['__pycache__', 'tests']:
files.add(f)
elif f[-3:] == '.py' and f != '__init__.py':
files.add(f[:-3])
@ -152,7 +154,8 @@ def importModules(path, globals, locals, excludes=()):
try:
if len(path) > 0:
modName = path + '.' + modName
mod = __import__(modName, globals, locals, fromlist=['*'])
#mod = __import__(modName, globals, locals, fromlist=['*'])
mod = __import__(modName, globals, locals, ['*'], 1)
mods[modName] = mod
except:
import traceback
@ -175,7 +178,8 @@ def importAll(path, globals, locals, excludes=()):
globals[k] = getattr(mod, k)
importAll('graphicsItems', globals(), locals())
importAll('widgets', globals(), locals(), excludes=['MatplotlibWidget', 'RemoteGraphicsView'])
importAll('widgets', globals(), locals(),
excludes=['MatplotlibWidget', 'RawImageWidget', 'RemoteGraphicsView'])
from .imageview import *
from .WidgetGroup import *
@ -190,9 +194,20 @@ from .SignalProxy import *
from .colormap import *
from .ptime import time
##############################################################
## PyQt and PySide both are prone to crashing on exit.
## There are two general approaches to dealing with this:
## 1. Install atexit handlers that assist in tearing down to avoid crashes.
## This helps, but is never perfect.
## 2. Terminate the process before python starts tearing down
## This is potentially dangerous
## Attempts to work around exit crashes:
import atexit
def cleanup():
if not getConfigOption('exitCleanup'):
return
ViewBox.quit() ## tell ViewBox that it doesn't need to deregister views anymore.
## Workaround for Qt exit crash:
@ -212,6 +227,38 @@ def cleanup():
atexit.register(cleanup)
## Optional function for exiting immediately (with some manual teardown)
def exit():
"""
Causes python to exit without garbage-collecting any objects, and thus avoids
calling object destructor methods. This is a sledgehammer workaround for
a variety of bugs in PyQt and Pyside that cause crashes on exit.
This function does the following in an attempt to 'safely' terminate
the process:
* Invoke atexit callbacks
* Close all open file handles
* os._exit()
Note: there is some potential for causing damage with this function if you
are using objects that _require_ their destructors to be called (for example,
to properly terminate log files, disconnect from devices, etc). Situations
like this are probably quite rare, but use at your own risk.
"""
## first disable our own cleanup function; won't be needing it.
setConfigOptions(exitCleanup=False)
## invoke atexit callbacks
atexit._run_exitfuncs()
## close file handles
os.closerange(3, 4096) ## just guessing on the maximum descriptor count..
os._exit(0)
## Convenience functions for command-line use
@ -235,7 +282,7 @@ def plot(*args, **kargs):
#if len(args)+len(kargs) > 0:
#w.plot(*args, **kargs)
pwArgList = ['title', 'labels', 'name', 'left', 'right', 'top', 'bottom']
pwArgList = ['title', 'labels', 'name', 'left', 'right', 'top', 'bottom', 'background']
pwArgs = {}
dataArgs = {}
for k in kargs:
@ -265,13 +312,15 @@ def image(*args, **kargs):
return w
show = image ## for backward compatibility
def dbg():
def dbg(*args, **kwds):
"""
Create a console window and begin watching for exceptions.
All arguments are passed to :func:`ConsoleWidget.__init__() <pyqtgraph.console.ConsoleWidget.__init__>`.
"""
mkQApp()
import console
c = console.ConsoleWidget()
from . import console
c = console.ConsoleWidget(*args, **kwds)
c.catchAllExceptions()
c.show()
global consoles

View File

@ -169,7 +169,7 @@ class ConsoleWidget(QtGui.QWidget):
def execMulti(self, nextLine):
self.stdout.write(nextLine+"\n")
#self.stdout.write(nextLine+"\n")
if nextLine.strip() != '':
self.multiline += "\n" + nextLine
return
@ -372,4 +372,4 @@ class ConsoleWidget(QtGui.QWidget):
return False
return True

View File

@ -28,6 +28,15 @@ def ftrace(func):
return rv
return w
def warnOnException(func):
"""Decorator which catches/ignores exceptions and prints a stack trace."""
def w(*args, **kwds):
try:
func(*args, **kwds)
except:
printExc('Ignored exception:')
return w
def getExc(indent=4, prefix='| '):
tb = traceback.format_exc()
lines = []

View File

@ -209,6 +209,13 @@ class Dock(QtGui.QWidget, DockDrop):
self.setOrientation(force=True)
def close(self):
"""Remove this dock from the DockArea it lives inside."""
self.setParent(None)
self.label.setParent(None)
self._container.apoptose()
self._container = None
def __repr__(self):
return "<Dock %s %s>" % (self.name(), self.stretch())

View File

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

View File

@ -1,6 +1,7 @@
from pyqtgraph.widgets.FileDialog import FileDialog
import pyqtgraph as pg
from pyqtgraph.Qt import QtGui, QtCore, QtSvg
from pyqtgraph.python2_3 import asUnicode
import os, re
LastExportDirectory = None
@ -56,13 +57,13 @@ class Exporter(object):
return
def fileSaveFinished(self, fileName):
fileName = str(fileName)
fileName = asUnicode(fileName)
global LastExportDirectory
LastExportDirectory = os.path.split(fileName)[0]
## If file name does not match selected extension, append it now
ext = os.path.splitext(fileName)[1].lower().lstrip('.')
selectedExt = re.search(r'\*\.(\w+)\b', str(self.fileDialog.selectedNameFilter()))
selectedExt = re.search(r'\*\.(\w+)\b', asUnicode(self.fileDialog.selectedNameFilter()))
if selectedExt is not None:
selectedExt = selectedExt.groups()[0].lower()
if ext != selectedExt:
@ -118,7 +119,7 @@ class Exporter(object):
else:
childs = root.childItems()
rootItem = [root]
childs.sort(lambda a,b: cmp(a.zValue(), b.zValue()))
childs.sort(key=lambda a: a.zValue())
while len(childs) > 0:
ch = childs.pop(0)
tree = self.getPaintItems(ch)

View File

@ -1,6 +1,6 @@
from .Exporter import Exporter
from pyqtgraph.parametertree import Parameter
from pyqtgraph.Qt import QtGui, QtCore, QtSvg
from pyqtgraph.Qt import QtGui, QtCore, QtSvg, USE_PYSIDE
import pyqtgraph as pg
import numpy as np
@ -17,7 +17,11 @@ class ImageExporter(Exporter):
scene = item.scene()
else:
scene = item
bg = scene.views()[0].backgroundBrush().color()
bgbrush = scene.views()[0].backgroundBrush()
bg = bgbrush.color()
if bgbrush.style() == QtCore.Qt.NoBrush:
bg.setAlpha(0)
self.params = Parameter(name='params', type='group', children=[
{'name': 'width', 'type': 'int', 'value': tr.width(), 'limits': (0, None)},
{'name': 'height', 'type': 'int', 'value': tr.height(), 'limits': (0, None)},
@ -42,7 +46,10 @@ class ImageExporter(Exporter):
def export(self, fileName=None, toBytes=False, copy=False):
if fileName is None and not toBytes and not copy:
filter = ["*."+str(f) for f in QtGui.QImageWriter.supportedImageFormats()]
if USE_PYSIDE:
filter = ["*."+str(f) for f in QtGui.QImageWriter.supportedImageFormats()]
else:
filter = ["*."+bytes(f).decode('utf-8') for f in QtGui.QImageWriter.supportedImageFormats()]
preferred = ['*.png', '*.tif', '*.jpg']
for p in preferred[::-1]:
if p in filter:
@ -57,6 +64,9 @@ class ImageExporter(Exporter):
#self.png = QtGui.QImage(targetRect.size(), QtGui.QImage.Format_ARGB32)
#self.png.fill(pyqtgraph.mkColor(self.params['background']))
w, h = self.params['width'], self.params['height']
if w == 0 or h == 0:
raise Exception("Cannot export image with size=0 (requested export size is %dx%d)" % (w,h))
bg = np.empty((self.params['width'], self.params['height'], 4), dtype=np.ubyte)
color = self.params['background']
bg[:,:,0] = color.blue()

View File

@ -1,4 +1,5 @@
from .Exporter import Exporter
from pyqtgraph.python2_3 import asUnicode
from pyqtgraph.parametertree import Parameter
from pyqtgraph.Qt import QtGui, QtCore, QtSvg
import pyqtgraph as pg
@ -91,8 +92,8 @@ class SVGExporter(Exporter):
md.setData('image/svg+xml', QtCore.QByteArray(xml.encode('UTF-8')))
QtGui.QApplication.clipboard().setMimeData(md)
else:
with open(fileName, 'w') as fh:
fh.write(xml.encode('UTF-8'))
with open(fileName, 'wb') as fh:
fh.write(asUnicode(xml).encode('utf-8'))
xmlHeader = """\
@ -221,8 +222,8 @@ def _generateItemSvg(item, nodes=None, root=None):
## this is taken care of in generateSvg instead.
#if hasattr(item, 'setExportMode'):
#item.setExportMode(False)
xmlStr = str(arr)
xmlStr = bytes(arr).decode('utf-8')
doc = xml.parseString(xmlStr)
try:
@ -304,14 +305,43 @@ def _generateItemSvg(item, nodes=None, root=None):
def correctCoordinates(node, item):
## Remove transformation matrices from <g> tags by applying matrix to coordinates inside.
## Each item is represented by a single top-level group with one or more groups inside.
## Each inner group contains one or more drawing primitives, possibly of different types.
groups = node.getElementsByTagName('g')
## Since we leave text unchanged, groups which combine text and non-text primitives must be split apart.
## (if at some point we start correcting text transforms as well, then it should be safe to remove this)
groups2 = []
for grp in groups:
subGroups = [grp.cloneNode(deep=False)]
textGroup = None
for ch in grp.childNodes[:]:
if isinstance(ch, xml.Element):
if textGroup is None:
textGroup = ch.tagName == 'text'
if ch.tagName == 'text':
if textGroup is False:
subGroups.append(grp.cloneNode(deep=False))
textGroup = True
else:
if textGroup is True:
subGroups.append(grp.cloneNode(deep=False))
textGroup = False
subGroups[-1].appendChild(ch)
groups2.extend(subGroups)
for sg in subGroups:
node.insertBefore(sg, grp)
node.removeChild(grp)
groups = groups2
for grp in groups:
matrix = grp.getAttribute('transform')
match = re.match(r'matrix\((.*)\)', matrix)
if match is None:
vals = [1,0,0,1,0,0]
else:
vals = map(float, match.groups()[0].split(','))
vals = [float(a) for a in match.groups()[0].split(',')]
tr = np.array([[vals[0], vals[2], vals[4]], [vals[1], vals[3], vals[5]]])
removeTransform = False
@ -320,9 +350,9 @@ def correctCoordinates(node, item):
continue
if ch.tagName == 'polyline':
removeTransform = True
coords = np.array([map(float, c.split(',')) for c in ch.getAttribute('points').strip().split(' ')])
coords = np.array([[float(a) for a in c.split(',')] for c in ch.getAttribute('points').strip().split(' ')])
coords = pg.transformCoordinates(tr, coords, transpose=True)
ch.setAttribute('points', ' '.join([','.join(map(str, c)) for c in coords]))
ch.setAttribute('points', ' '.join([','.join([str(a) for a in c]) for c in coords]))
elif ch.tagName == 'path':
removeTransform = True
newCoords = ''
@ -374,7 +404,6 @@ def correctCoordinates(node, item):
if removeTransform:
grp.removeAttribute('transform')
def itemTransform(item, root):
## Return the transformation mapping item to root

View File

@ -376,10 +376,10 @@ class Flowchart(Node):
#tdeps[t] = lastNode
if lastInd is not None:
dels.append((lastInd+1, t))
dels.sort(lambda a,b: cmp(b[0], a[0]))
#dels.sort(lambda a,b: cmp(b[0], a[0]))
dels.sort(key=lambda a: a[0], reverse=True)
for i, t in dels:
ops.insert(i, ('d', t))
return ops
@ -491,7 +491,8 @@ class Flowchart(Node):
self.clear()
Node.restoreState(self, state)
nodes = state['nodes']
nodes.sort(lambda a, b: cmp(a['pos'][0], b['pos'][0]))
#nodes.sort(lambda a, b: cmp(a['pos'][0], b['pos'][0]))
nodes.sort(key=lambda a: a['pos'][0])
for n in nodes:
if n['name'] in self._nodes:
#self._nodes[n['name']].graphicsItem().moveBy(*n['pos'])
@ -560,6 +561,7 @@ class Flowchart(Node):
self.fileDialog.fileSelected.connect(self.saveFile)
return
#fileName = QtGui.QFileDialog.getSaveFileName(None, "Save Flowchart..", startDir, "Flowchart (*.fc)")
fileName = str(fileName)
configfile.writeConfigFile(self.saveState(), fileName)
self.sigFileSaved.emit(fileName)
@ -681,7 +683,7 @@ class FlowchartCtrlWidget(QtGui.QWidget):
#self.setCurrentFile(newFile)
def fileSaved(self, fileName):
self.setCurrentFile(fileName)
self.setCurrentFile(str(fileName))
self.ui.saveBtn.success("Saved.")
def saveClicked(self):
@ -710,7 +712,7 @@ class FlowchartCtrlWidget(QtGui.QWidget):
#self.setCurrentFile(newFile)
def setCurrentFile(self, fileName):
self.currentFileName = fileName
self.currentFileName = str(fileName)
if fileName is None:
self.ui.fileNameLabel.setText("<b>[ new ]</b>")
else:

View File

@ -521,6 +521,17 @@ class ConnectionItem(GraphicsObject):
self.target = target
self.length = 0
self.hovered = False
self.path = None
self.shapePath = None
self.style = {
'shape': 'line',
'color': (100, 100, 250),
'width': 1.0,
'hoverColor': (150, 150, 250),
'hoverWidth': 1.0,
'selectedColor': (200, 200, 0),
'selectedWidth': 3.0,
}
#self.line = QtGui.QGraphicsLineItem(self)
self.source.getViewBox().addItem(self)
self.updateLine()
@ -535,6 +546,13 @@ class ConnectionItem(GraphicsObject):
self.target = target
self.updateLine()
def setStyle(self, **kwds):
self.style.update(kwds)
if 'shape' in kwds:
self.updateLine()
else:
self.update()
def updateLine(self):
start = Point(self.source.connectPoint())
if isinstance(self.target, TerminalGraphicsItem):
@ -544,15 +562,21 @@ class ConnectionItem(GraphicsObject):
else:
return
self.prepareGeometryChange()
self.resetTransform()
ang = (stop-start).angle(Point(0, 1))
if ang is None:
ang = 0
self.rotate(ang)
self.setPos(start)
self.length = (start-stop).length()
self.path = self.generatePath(start, stop)
self.shapePath = None
self.update()
#self.line.setLine(start.x(), start.y(), stop.x(), stop.y())
def generatePath(self, start, stop):
path = QtGui.QPainterPath()
path.moveTo(start)
if self.style['shape'] == 'line':
path.lineTo(stop)
elif self.style['shape'] == 'cubic':
path.cubicTo(Point(stop.x(), start.y()), Point(start.x(), stop.y()), Point(stop.x(), stop.y()))
else:
raise Exception('Invalid shape "%s"; options are "line" or "cubic"' % self.style['shape'])
return path
def keyPressEvent(self, ev):
if ev.key() == QtCore.Qt.Key_Delete or ev.key() == QtCore.Qt.Key_Backspace:
@ -582,20 +606,33 @@ class ConnectionItem(GraphicsObject):
def boundingRect(self):
#return self.line.boundingRect()
px = self.pixelWidth()
return QtCore.QRectF(-5*px, 0, 10*px, self.length)
return self.shape().boundingRect()
##return self.line.boundingRect()
#px = self.pixelWidth()
#return QtCore.QRectF(-5*px, 0, 10*px, self.length)
def viewRangeChanged(self):
self.shapePath = None
self.prepareGeometryChange()
#def shape(self):
#return self.line.shape()
def shape(self):
if self.shapePath is None:
if self.path is None:
return QtGui.QPainterPath()
stroker = QtGui.QPainterPathStroker()
px = self.pixelWidth()
stroker.setWidth(px*8)
self.shapePath = stroker.createStroke(self.path)
return self.shapePath
def paint(self, p, *args):
if self.isSelected():
p.setPen(fn.mkPen(200, 200, 0, width=3))
p.setPen(fn.mkPen(self.style['selectedColor'], width=self.style['selectedWidth']))
else:
if self.hovered:
p.setPen(fn.mkPen(150, 150, 250, width=1))
p.setPen(fn.mkPen(self.style['hoverColor'], width=self.style['hoverWidth']))
else:
p.setPen(fn.mkPen(100, 100, 250, width=1))
p.setPen(fn.mkPen(self.style['color'], width=self.style['width']))
p.drawLine(0, 0, 0, self.length)
#p.drawLine(0, 0, 0, self.length)
p.drawPath(self.path)

View File

@ -24,7 +24,15 @@ class BinOpNode(Node):
})
def process(self, **args):
fn = getattr(args['A'], self.fn)
if isinstance(self.fn, tuple):
for name in self.fn:
try:
fn = getattr(args['A'], name)
break
except AttributeError:
pass
else:
fn = getattr(args['A'], self.fn)
out = fn(args['B'])
if out is NotImplemented:
raise Exception("Operation %s not implemented between %s and %s" % (fn, str(type(args['A'])), str(type(args['B']))))
@ -60,5 +68,7 @@ class DivideNode(BinOpNode):
"""Returns A / B. Does not check input types."""
nodeName = 'Divide'
def __init__(self, name):
BinOpNode.__init__(self, name, '__div__')
# try truediv first, followed by div
BinOpNode.__init__(self, name, ('__truediv__', '__div__'))

View File

@ -5,6 +5,7 @@ Copyright 2010 Luke Campagnola
Distributed under MIT/X11 license. See license.txt for more infomation.
"""
from __future__ import division
from .python2_3 import asUnicode
Colors = {
'b': (0,0,255,255),
@ -23,7 +24,7 @@ SI_PREFIXES_ASCII = 'yzafpnum kMGTPEZY'
from .Qt import QtGui, QtCore, USE_PYSIDE
from pyqtgraph import getConfigOption
import pyqtgraph as pg
import numpy as np
import decimal, re
import ctypes
@ -32,12 +33,11 @@ import sys, struct
try:
import scipy.ndimage
HAVE_SCIPY = True
WEAVE_DEBUG = getConfigOption('weaveDebug')
try:
import scipy.weave
USE_WEAVE = getConfigOption('useWeave')
except:
USE_WEAVE = False
if pg.getConfigOption('useWeave'):
try:
import scipy.weave
except ImportError:
pg.setConfigOptions(useWeave=False)
except ImportError:
HAVE_SCIPY = False
@ -264,6 +264,7 @@ def mkPen(*args, **kargs):
color = kargs.get('color', None)
width = kargs.get('width', 1)
style = kargs.get('style', None)
dash = kargs.get('dash', None)
cosmetic = kargs.get('cosmetic', True)
hsv = kargs.get('hsv', None)
@ -291,6 +292,8 @@ def mkPen(*args, **kargs):
pen.setCosmetic(cosmetic)
if style is not None:
pen.setStyle(style)
if dash is not None:
pen.setDashPattern(dash)
return pen
def hsvColor(hue, sat=1.0, val=1.0, alpha=1.0):
@ -611,15 +614,24 @@ def rescaleData(data, scale, offset, dtype=None):
Uses scipy.weave (if available) to improve performance.
"""
global USE_WEAVE
if dtype is None:
dtype = data.dtype
else:
dtype = np.dtype(dtype)
try:
if not USE_WEAVE:
if not pg.getConfigOption('useWeave'):
raise Exception('Weave is disabled; falling back to slower version.')
newData = np.empty((data.size,), dtype=dtype)
## require native dtype when using weave
if not data.dtype.isnative:
data = data.astype(data.dtype.newbyteorder('='))
if not dtype.isnative:
weaveDtype = dtype.newbyteorder('=')
else:
weaveDtype = dtype
newData = np.empty((data.size,), dtype=weaveDtype)
flat = np.ascontiguousarray(data).reshape(data.size)
size = data.size
@ -631,12 +643,14 @@ def rescaleData(data, scale, offset, dtype=None):
}
"""
scipy.weave.inline(code, ['flat', 'newData', 'size', 'offset', 'scale'], compiler='gcc')
if dtype != weaveDtype:
newData = newData.astype(dtype)
data = newData.reshape(data.shape)
except:
if USE_WEAVE:
if WEAVE_DEBUG:
if pg.getConfigOption('useWeave'):
if pg.getConfigOption('weaveDebug'):
debug.printExc("Error; disabling weave.")
USE_WEAVE = False
pg.setConfigOption('useWeave', False)
#p = np.poly1d([scale, -offset*scale])
#data = p(data).astype(dtype)
@ -653,8 +667,6 @@ def applyLookupTable(data, lut):
Uses scipy.weave to improve performance if it is available.
Note: color gradient lookup tables can be generated using GradientWidget.
"""
global USE_WEAVE
if data.dtype.kind not in ('i', 'u'):
data = data.astype(int)
@ -839,7 +851,6 @@ def makeARGB(data, lut=None, levels=None, scale=None, useRGBA=False):
if minVal == maxVal:
maxVal += 1e-16
data = rescaleData(data, scale/(maxVal-minVal), minVal, dtype=int)
prof.mark('2')
@ -849,7 +860,6 @@ def makeARGB(data, lut=None, levels=None, scale=None, useRGBA=False):
else:
if data.dtype is not np.ubyte:
data = np.clip(data, 0, 255).astype(np.ubyte)
prof.mark('3')
@ -904,7 +914,8 @@ def makeQImage(imgData, alpha=None, copy=True, transpose=True):
array.shape[2] == 4.
copy If True, the data is copied before converting to QImage.
If False, the new QImage points directly to the data in the array.
Note that the array must be contiguous for this to work.
Note that the array must be contiguous for this to work
(see numpy.ascontiguousarray).
transpose If True (the default), the array x/y axes are transposed before
creating the image. Note that Qt expects the axes to be in
(height, width) order whereas pyqtgraph usually prefers the
@ -954,12 +965,22 @@ def makeQImage(imgData, alpha=None, copy=True, transpose=True):
#addr = ctypes.addressof(ctypes.c_char.from_buffer(imgData, 0))
## PyQt API for QImage changed between 4.9.3 and 4.9.6 (I don't know exactly which version it was)
## So we first attempt the 4.9.6 API, then fall back to 4.9.3
addr = ctypes.c_char.from_buffer(imgData, 0)
#addr = ctypes.c_char.from_buffer(imgData, 0)
#try:
#img = QtGui.QImage(addr, imgData.shape[1], imgData.shape[0], imgFormat)
#except TypeError:
#addr = ctypes.addressof(addr)
#img = QtGui.QImage(addr, imgData.shape[1], imgData.shape[0], imgFormat)
try:
img = QtGui.QImage(addr, imgData.shape[1], imgData.shape[0], imgFormat)
except TypeError:
addr = ctypes.addressof(addr)
img = QtGui.QImage(addr, imgData.shape[1], imgData.shape[0], imgFormat)
img = QtGui.QImage(imgData.ctypes.data, imgData.shape[1], imgData.shape[0], imgFormat)
except:
if copy:
# does not leak memory, is not mutable
img = QtGui.QImage(buffer(imgData), imgData.shape[1], imgData.shape[0], imgFormat)
else:
# mutable, but leaks memory
img = QtGui.QImage(memoryview(imgData), imgData.shape[1], imgData.shape[0], imgFormat)
img.data = imgData
return img
#try:
@ -1056,14 +1077,29 @@ def arrayToQPath(x, y, connect='all'):
should be connected, or an array of int32 values (0 or 1) indicating
connections.
"""
## Create all vertices in path. The method used below creates a binary format so that all
## vertices can be read in at once. This binary format may change in future versions of Qt,
## Create all vertices in path. The method used below creates a binary format so that all
## vertices can be read in at once. This binary format may change in future versions of Qt,
## so the original (slower) method is left here for emergencies:
#path.moveTo(x[0], y[0])
#for i in range(1, y.shape[0]):
# path.lineTo(x[i], y[i])
#path.moveTo(x[0], y[0])
#if connect == 'all':
#for i in range(1, y.shape[0]):
#path.lineTo(x[i], y[i])
#elif connect == 'pairs':
#for i in range(1, y.shape[0]):
#if i%2 == 0:
#path.lineTo(x[i], y[i])
#else:
#path.moveTo(x[i], y[i])
#elif isinstance(connect, np.ndarray):
#for i in range(1, y.shape[0]):
#if connect[i] == 1:
#path.lineTo(x[i], y[i])
#else:
#path.moveTo(x[i], y[i])
#else:
#raise Exception('connect argument must be "all", "pairs", or array')
## Speed this up using >> operator
## Format is:
## numVerts(i4) 0(i4)
@ -1073,71 +1109,62 @@ def arrayToQPath(x, y, connect='all'):
## 0(i4)
##
## All values are big endian--pack using struct.pack('>d') or struct.pack('>i')
path = QtGui.QPainterPath()
#prof = debug.Profiler('PlotCurveItem.generatePath', disabled=True)
if sys.version_info[0] == 2: ## So this is disabled for python 3... why??
n = x.shape[0]
# create empty array, pad with extra space on either end
arr = np.empty(n+2, dtype=[('x', '>f8'), ('y', '>f8'), ('c', '>i4')])
# write first two integers
#prof.mark('allocate empty')
arr.data[12:20] = struct.pack('>ii', n, 0)
#prof.mark('pack header')
# Fill array with vertex values
arr[1:-1]['x'] = x
arr[1:-1]['y'] = y
# decide which points are connected by lines
if connect == 'pairs':
connect = np.empty((n/2,2), dtype=np.int32)
connect[:,0] = 1
connect[:,1] = 0
connect = connect.flatten()
if connect == 'all':
arr[1:-1]['c'] = 1
elif isinstance(connect, np.ndarray):
arr[1:-1]['c'] = connect
else:
raise Exception('connect argument must be "all", "pairs", or array')
#prof.mark('fill array')
# write last 0
lastInd = 20*(n+1)
arr.data[lastInd:lastInd+4] = struct.pack('>i', 0)
#prof.mark('footer')
# create datastream object and stream into path
buf = QtCore.QByteArray(arr.data[12:lastInd+4]) # I think one unnecessary copy happens here
#prof.mark('create buffer')
ds = QtCore.QDataStream(buf)
#prof.mark('create datastream')
ds >> path
#prof.mark('load')
#prof.finish()
n = x.shape[0]
# create empty array, pad with extra space on either end
arr = np.empty(n+2, dtype=[('x', '>f8'), ('y', '>f8'), ('c', '>i4')])
# write first two integers
#prof.mark('allocate empty')
byteview = arr.view(dtype=np.ubyte)
byteview[:12] = 0
byteview.data[12:20] = struct.pack('>ii', n, 0)
#prof.mark('pack header')
# Fill array with vertex values
arr[1:-1]['x'] = x
arr[1:-1]['y'] = y
# decide which points are connected by lines
if connect == 'pairs':
connect = np.empty((n/2,2), dtype=np.int32)
connect[:,0] = 1
connect[:,1] = 0
connect = connect.flatten()
if connect == 'finite':
connect = np.isfinite(x) & np.isfinite(y)
arr[1:-1]['c'] = connect
if connect == 'all':
arr[1:-1]['c'] = 1
elif isinstance(connect, np.ndarray):
arr[1:-1]['c'] = connect
else:
## This does exactly the same as above, but less efficiently (and more simply).
path.moveTo(x[0], y[0])
if connect == 'all':
for i in range(1, y.shape[0]):
path.lineTo(x[i], y[i])
elif connect == 'pairs':
for i in range(1, y.shape[0]):
if i%2 == 0:
path.lineTo(x[i], y[i])
else:
path.moveTo(x[i], y[i])
elif isinstance(connect, np.ndarray):
for i in range(1, y.shape[0]):
if connect[i] == 1:
path.lineTo(x[i], y[i])
else:
path.moveTo(x[i], y[i])
else:
raise Exception('connect argument must be "all", "pairs", or array')
raise Exception('connect argument must be "all", "pairs", or array')
#prof.mark('fill array')
# write last 0
lastInd = 20*(n+1)
byteview.data[lastInd:lastInd+4] = struct.pack('>i', 0)
#prof.mark('footer')
# create datastream object and stream into path
## Avoiding this method because QByteArray(str) leaks memory in PySide
#buf = QtCore.QByteArray(arr.data[12:lastInd+4]) # I think one unnecessary copy happens here
path.strn = byteview.data[12:lastInd+4] # make sure data doesn't run away
try:
buf = QtCore.QByteArray.fromRawData(path.strn)
except TypeError:
buf = QtCore.QByteArray(bytes(path.strn))
#prof.mark('create buffer')
ds = QtCore.QDataStream(buf)
ds >> path
#prof.mark('load')
#prof.finish()
return path
#def isosurface(data, level):
@ -1838,9 +1865,9 @@ def isosurface(data, level):
for i in [0,1,2]:
vim = vertexInds[:,3] == i
vi = vertexInds[vim, :3]
viFlat = (vi * (np.array(data.strides[:3]) / data.itemsize)[np.newaxis,:]).sum(axis=1)
viFlat = (vi * (np.array(data.strides[:3]) // data.itemsize)[np.newaxis,:]).sum(axis=1)
v1 = dataFlat[viFlat]
v2 = dataFlat[viFlat + data.strides[i]/data.itemsize]
v2 = dataFlat[viFlat + data.strides[i]//data.itemsize]
vertexes[vim,i] += (level-v1) / (v2-v1)
### compute the set of vertex indexes for each face.
@ -1866,7 +1893,7 @@ def isosurface(data, level):
#p = debug.Profiler('isosurface', disabled=False)
## this helps speed up an indexing operation later on
cs = np.array(cutEdges.strides)/cutEdges.itemsize
cs = np.array(cutEdges.strides)//cutEdges.itemsize
cutEdges = cutEdges.flatten()
## this, strangely, does not seem to help.
@ -1925,9 +1952,9 @@ def invertQTransform(tr):
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):
def pseudoScatter(data, spacing=None, shuffle=True, bidir=False):
"""
Used for examining the distribution of values in a set.
Used for examining the distribution of values in a set. Produces scattering as in beeswarm or column scatter plots.
Given a list of x-values, construct a set of y-values such that an x,y scatter-plot
will not have overlapping points (it will look similar to a histogram).
@ -1943,6 +1970,8 @@ def pseudoScatter(data, spacing=None, shuffle=True):
s2 = spacing**2
yvals = np.empty(len(data))
if len(data) == 0:
return yvals
yvals[0] = 0
for i in range(1,len(data)):
x = data[i] # current x value to be placed
@ -1954,23 +1983,41 @@ def pseudoScatter(data, spacing=None, shuffle=True):
xmask = dx < s2 # exclude anything too far away
if xmask.sum() > 0:
dx = dx[xmask]
dy = (s2 - dx)**0.5
limits = np.empty((2,len(dy))) # ranges of y-values to exclude
limits[0] = y0[xmask] - dy
limits[1] = y0[xmask] + dy
while True:
# ignore anything below this y-value
mask = limits[1] >= y
limits = limits[:,mask]
# are we inside an excluded region?
mask = (limits[0] < y) & (limits[1] > y)
if mask.sum() == 0:
break
y = limits[:,mask].max()
if bidir:
dirs = [-1, 1]
else:
dirs = [1]
yopts = []
for direction in dirs:
y = 0
dx2 = dx[xmask]
dy = (s2 - dx2)**0.5
limits = np.empty((2,len(dy))) # ranges of y-values to exclude
limits[0] = y0[xmask] - dy
limits[1] = y0[xmask] + dy
while True:
# ignore anything below this y-value
if direction > 0:
mask = limits[1] >= y
else:
mask = limits[0] <= y
limits2 = limits[:,mask]
# are we inside an excluded region?
mask = (limits2[0] < y) & (limits2[1] > y)
if mask.sum() == 0:
break
if direction > 0:
y = limits2[:,mask].max()
else:
y = limits2[:,mask].min()
yopts.append(y)
if bidir:
y = yopts[0] if -yopts[0] < yopts[1] else yopts[1]
else:
y = yopts[0]
yvals[i] = y
return yvals[np.argsort(inds)] ## un-shuffle values before returning

View File

@ -39,19 +39,25 @@ class AxisItem(GraphicsWidget):
if orientation not in ['left', 'right', 'top', 'bottom']:
raise Exception("Orientation argument must be one of 'left', 'right', 'top', or 'bottom'.")
if orientation in ['left', 'right']:
#self.setMinimumWidth(25)
#self.setSizePolicy(QtGui.QSizePolicy(
#QtGui.QSizePolicy.Minimum,
#QtGui.QSizePolicy.Expanding
#))
self.label.rotate(-90)
#else:
#self.setMinimumHeight(50)
#self.setSizePolicy(QtGui.QSizePolicy(
#QtGui.QSizePolicy.Expanding,
#QtGui.QSizePolicy.Minimum
#))
#self.drawLabel = False
self.style = {
'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
'tickFont': None,
'stopAxisAtTick': (False, False), ## whether axis is drawn to edge of box or to last tick
'textFillLimits': [ ## how much of the axis to fill up with tick text, maximally.
(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
]
}
self.textWidth = 30 ## Keeps track of maximum width / height of tick text
self.textHeight = 18
self.labelText = ''
self.labelUnits = ''
@ -60,11 +66,11 @@ class AxisItem(GraphicsWidget):
self.logMode = False
self.tickFont = None
self.textHeight = 18
self.tickLength = maxTickLength
self._tickLevels = None ## used to override the automatic ticking system with explicit ticks
self.scale = 1.0
self.autoScale = True
self.autoSIPrefix = True
self.autoSIPrefixScale = 1.0
self.setRange(0, 1)
@ -144,8 +150,8 @@ class AxisItem(GraphicsWidget):
self.setWidth()
else:
self.setHeight()
if self.autoScale:
self.setScale()
if self.autoSIPrefix:
self.updateAutoSIPrefix()
def setLabel(self, text=None, units=None, unitPrefix=None, **args):
"""Set the text displayed adjacent to the axis.
@ -184,33 +190,62 @@ class AxisItem(GraphicsWidget):
if len(args) > 0:
self.labelStyle = args
self.label.setHtml(self.labelString())
self.resizeEvent()
self._adjustSize()
self.picture = None
self.update()
def labelString(self):
if self.labelUnits == '':
if self.scale == 1.0:
if not self.autoSIPrefix or self.autoSIPrefixScale == 1.0:
units = ''
else:
units = asUnicode('(x%g)') % (1.0/self.scale)
units = asUnicode('(x%g)') % (1.0/self.autoSIPrefixScale)
else:
#print repr(self.labelUnitPrefix), repr(self.labelUnits)
units = asUnicode('(%s%s)') % (self.labelUnitPrefix, self.labelUnits)
units = asUnicode('(%s%s)') % (asUnicode(self.labelUnitPrefix), asUnicode(self.labelUnits))
s = asUnicode('%s %s') % (self.labelText, units)
s = asUnicode('%s %s') % (asUnicode(self.labelText), asUnicode(units))
style = ';'.join(['%s: %s' % (k, self.labelStyle[k]) for k in self.labelStyle])
return asUnicode("<span style='%s'>%s</span>") % (style, s)
return asUnicode("<span style='%s'>%s</span>") % (style, asUnicode(s))
def _updateMaxTextSize(self, x):
## Informs that the maximum tick size orthogonal to the axis has
## changed; we use this to decide whether the item needs to be resized
## to accomodate.
if self.orientation in ['left', 'right']:
mx = max(self.textWidth, x)
if mx > self.textWidth or mx < self.textWidth-10:
self.textWidth = mx
if self.style['autoExpandTextSpace'] is True:
self.setWidth()
#return True ## size has changed
else:
mx = max(self.textHeight, x)
if mx > self.textHeight or mx < self.textHeight-10:
self.textHeight = mx
if self.style['autoExpandTextSpace'] is True:
self.setHeight()
#return True ## size has changed
def _adjustSize(self):
if self.orientation in ['left', 'right']:
self.setWidth()
else:
self.setHeight()
def setHeight(self, h=None):
"""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:
h = self.textHeight + max(0, self.tickLength)
if self.style['autoExpandTextSpace'] is True:
h = self.textHeight
else:
h = self.style['tickTextHeight']
h += max(0, self.tickLength) + self.style['tickTextOffset'][1]
if self.label.isVisible():
h += self.textHeight
h += self.label.boundingRect().height() * 0.8
self.setMaximumHeight(h)
self.setMinimumHeight(h)
self.picture = None
@ -220,11 +255,16 @@ class AxisItem(GraphicsWidget):
"""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:
w = max(0, self.tickLength) + 40
if self.style['autoExpandTextSpace'] is True:
w = self.textWidth
else:
w = self.style['tickTextWidth']
w += max(0, self.tickLength) + self.style['tickTextOffset'][0]
if self.label.isVisible():
w += self.textHeight
w += self.label.boundingRect().height() * 0.8 ## bounding rect is usually an overestimate
self.setMaximumWidth(w)
self.setMinimumWidth(w)
self.picture = None
def pen(self):
if self._pen is None:
@ -247,30 +287,16 @@ class AxisItem(GraphicsWidget):
def setScale(self, scale=None):
"""
Set the value scaling for this axis. Values on the axis are multiplied
by this scale factor before being displayed as text. By default,
this scaling value is automatically determined based on the visible range
and the axis units are updated to reflect the chosen scale factor.
Set the value scaling for this axis.
For example: If the axis spans values from -0.1 to 0.1 and has units set
to 'V' then a scale of 1000 would cause the axis to display values -100 to 100
and the units would appear as 'mV'
Setting this value causes the axis to draw ticks and tick labels as if
the view coordinate system were scaled. By default, the axis scaling is
1.0.
"""
if scale is None:
#if self.drawLabel: ## If there is a label, then we are free to rescale the values
if self.label.isVisible():
#d = self.range[1] - self.range[0]
#(scale, prefix) = fn.siScale(d / 2.)
(scale, prefix) = fn.siScale(max(abs(self.range[0]), abs(self.range[1])))
if self.labelUnits == '' and prefix in ['k', 'm']: ## If we are not showing units, wait until 1e6 before scaling.
scale = 1.0
prefix = ''
self.setLabel(unitPrefix=prefix)
else:
scale = 1.0
else:
self.setLabel(unitPrefix='')
self.autoScale = False
# Deprecated usage, kept for backward compatibility
if scale is None:
scale = 1.0
self.enableAutoSIPrefix(True)
if scale != self.scale:
self.scale = scale
@ -278,14 +304,47 @@ class AxisItem(GraphicsWidget):
self.picture = None
self.update()
def enableAutoSIPrefix(self, enable=True):
"""
Enable (or disable) automatic SI prefix scaling on this axis.
When enabled, this feature automatically determines the best SI prefix
to prepend to the label units, while ensuring that axis values are scaled
accordingly.
For example, if the axis spans values from -0.1 to 0.1 and has units set
to 'V' then the axis would display values -100 to 100
and the units would appear as 'mV'
This feature is enabled by default, and is only available when a suffix
(unit string) is provided to display on the label.
"""
self.autoSIPrefix = enable
self.updateAutoSIPrefix()
def updateAutoSIPrefix(self):
if self.label.isVisible():
(scale, prefix) = fn.siScale(max(abs(self.range[0]*self.scale), abs(self.range[1]*self.scale)))
if self.labelUnits == '' and prefix in ['k', 'm']: ## If we are not showing units, wait until 1e6 before scaling.
scale = 1.0
prefix = ''
self.setLabel(unitPrefix=prefix)
else:
scale = 1.0
self.autoSIPrefixScale = scale
self.picture = None
self.update()
def setRange(self, mn, mx):
"""Set the range of values displayed by the axis.
Usually this is handled automatically by linking the axis to a ViewBox with :func:`linkToView <pyqtgraph.AxisItem.linkToView>`"""
if any(np.isinf((mn, mx))) or any(np.isnan((mn, mx))):
raise Exception("Not setting range to [%s, %s]" % (str(mn), str(mx)))
self.range = [mn, mx]
if self.autoScale:
self.setScale()
if self.autoSIPrefix:
self.updateAutoSIPrefix()
self.picture = None
self.update()
@ -309,10 +368,21 @@ class AxisItem(GraphicsWidget):
oldView.sigXRangeChanged.disconnect(self.linkedViewChanged)
view.sigXRangeChanged.connect(self.linkedViewChanged)
def linkedViewChanged(self, view, newRange):
if self.orientation in ['right', 'left'] and view.yInverted():
self.setRange(*newRange[::-1])
if oldView is not None:
oldView.sigResized.disconnect(self.linkedViewChanged)
view.sigResized.connect(self.linkedViewChanged)
def linkedViewChanged(self, view, newRange=None):
if self.orientation in ['right', 'left']:
if newRange is None:
newRange = view.viewRange()[1]
if view.yInverted():
self.setRange(*newRange[::-1])
else:
self.setRange(*newRange)
else:
if newRange is None:
newRange = view.viewRange()[0]
self.setRange(*newRange)
def boundingRect(self):
@ -322,34 +392,36 @@ class AxisItem(GraphicsWidget):
## extend rect if ticks go in negative direction
## also extend to account for text that flows past the edges
if self.orientation == 'left':
#rect.setRight(rect.right() - min(0,self.tickLength))
#rect.setTop(rect.top() - 15)
#rect.setBottom(rect.bottom() + 15)
rect = rect.adjusted(0, -15, -min(0,self.tickLength), 15)
elif self.orientation == 'right':
#rect.setLeft(rect.left() + min(0,self.tickLength))
rect = rect.adjusted(min(0,self.tickLength), -15, 0, 15)
elif self.orientation == 'top':
#rect.setBottom(rect.bottom() - min(0,self.tickLength))
rect = rect.adjusted(-15, 0, 15, -min(0,self.tickLength))
elif self.orientation == 'bottom':
#rect.setTop(rect.top() + min(0,self.tickLength))
rect = rect.adjusted(-15, min(0,self.tickLength), 15, 0)
return rect
else:
return self.mapRectFromParent(self.geometry()) | linkedView.mapRectToItem(self, linkedView.boundingRect())
def paint(self, p, opt, widget):
prof = debug.Profiler('AxisItem.paint', disabled=True)
if self.picture is None:
self.picture = QtGui.QPicture()
painter = QtGui.QPainter(self.picture)
try:
self.drawPicture(painter)
picture = QtGui.QPicture()
painter = QtGui.QPainter(picture)
specs = self.generateDrawSpecs(painter)
prof.mark('generate specs')
if specs is not None:
self.drawPicture(painter, *specs)
prof.mark('draw picture')
finally:
painter.end()
self.picture = picture
#p.setRenderHint(p.Antialiasing, False) ## Sometimes we get a segfault here ???
#p.setRenderHint(p.TextAntialiasing, True)
self.picture.play(p)
prof.finish()
def setTicks(self, ticks):
@ -375,7 +447,7 @@ class AxisItem(GraphicsWidget):
This method is called whenever the axis needs to be redrawn and is a
good method to override in subclasses that require control over tick locations.
The return value must be a list of three tuples::
The return value must be a list of tuples, one for each set of ticks::
[
(major tick spacing, offset),
@ -389,7 +461,7 @@ class AxisItem(GraphicsWidget):
return []
## decide optimal minor tick spacing in pixels (this is just aesthetics)
pixelSpacing = np.log(size+10) * 5
pixelSpacing = size / np.log(size)
optimalTickCount = max(2., size / pixelSpacing)
## optimal minor tick spacing
@ -458,6 +530,10 @@ class AxisItem(GraphicsWidget):
"""
minVal, maxVal = sorted((minVal, maxVal))
minVal *= self.scale
maxVal *= self.scale
#size *= self.scale
ticks = []
tickLevels = self.tickSpacing(minVal, maxVal, size)
@ -470,16 +546,25 @@ class AxisItem(GraphicsWidget):
## determine number of ticks
num = int((maxVal-start) / spacing) + 1
values = np.arange(num) * spacing + start
values = (np.arange(num) * spacing + start) / self.scale
## remove any ticks that were present in higher levels
## we assume here that if the difference between a tick value and a previously seen tick value
## is less than spacing/100, then they are 'equal' and we can ignore the new tick.
values = list(filter(lambda x: all(np.abs(allValues-x) > spacing*0.01), values) )
allValues = np.concatenate([allValues, values])
ticks.append((spacing, values))
ticks.append((spacing/self.scale, values))
if self.logMode:
return self.logTickValues(minVal, maxVal, size, ticks)
#nticks = []
#for t in ticks:
#nvals = []
#for v in t[1]:
#nvals.append(v/self.scale)
#nticks.append((t[0]/self.scale,nvals))
#ticks = nticks
return ticks
@ -535,12 +620,13 @@ class AxisItem(GraphicsWidget):
def logTickStrings(self, values, scale, spacing):
return ["%0.1g"%x for x in 10 ** np.array(values).astype(float)]
def drawPicture(self, p):
p.setRenderHint(p.Antialiasing, False)
p.setRenderHint(p.TextAntialiasing, True)
prof = debug.Profiler("AxisItem.paint", disabled=True)
def generateDrawSpecs(self, p):
"""
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().
"""
prof = debug.Profiler("AxisItem.generateDrawSpecs", disabled=True)
#bounds = self.boundingRect()
bounds = self.mapRectFromParent(self.geometry())
@ -577,11 +663,6 @@ class AxisItem(GraphicsWidget):
axis = 1
#print tickStart, tickStop, span
## draw long line along axis
p.setPen(self.pen())
p.drawLine(*span)
p.translate(0.5,0) ## resolves some damn pixel ambiguity
## determine size of this item in pixels
points = list(map(self.mapToDevice, span))
if None in points:
@ -610,12 +691,16 @@ class AxisItem(GraphicsWidget):
## determine mapping between tick values and local coordinates
dif = self.range[1] - self.range[0]
if axis == 0:
xScale = -bounds.height() / dif
offset = self.range[0] * xScale - bounds.height()
if dif == 0:
xscale = 1
offset = 0
else:
xScale = bounds.width() / dif
offset = self.range[0] * xScale
if axis == 0:
xScale = -bounds.height() / dif
offset = self.range[0] * xScale - bounds.height()
else:
xScale = bounds.width() / dif
offset = self.range[0] * xScale
xRange = [x * xScale - offset for x in self.range]
xMin = min(xRange)
@ -628,7 +713,7 @@ class AxisItem(GraphicsWidget):
## draw ticks
## (to improve performance, we do not interleave line and text drawing, since this causes unnecessary pipeline switching)
## draw three different intervals, long ticks first
tickSpecs = []
for i in range(len(tickLevels)):
tickPositions.append([])
ticks = tickLevels[i][1]
@ -658,20 +743,44 @@ class AxisItem(GraphicsWidget):
color = tickPen.color()
color.setAlpha(lineAlpha)
tickPen.setColor(color)
p.setPen(tickPen)
p.drawLine(Point(p1), Point(p2))
prof.mark('draw ticks')
tickSpecs.append((tickPen, Point(p1), Point(p2)))
prof.mark('compute ticks')
## Draw text until there is no more room (or no more text)
if self.tickFont is not None:
p.setFont(self.tickFont)
## 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)))
if axis == 0:
span[0].setY(stop)
else:
span[0].setX(stop)
if self.style['stopAxisAtTick'][1] is True:
stop = min(span[1].y(), max(map(max, tickPositions)))
if axis == 0:
span[1].setY(stop)
else:
span[1].setX(stop)
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
#textHeight = self.textHeight
#else:
#textWidth = self.style['tickTextWidth'] ## space allocated for horizontal text
#textHeight = self.style['tickTextHeight'] ## space allocated for horizontal text
textSize2 = 0
textRects = []
textSpecs = [] ## list of draw
textSize2 = 0
for i in range(len(tickLevels)):
## Get the list of strings to display for this level
if tickStrings is None:
spacing, values = tickLevels[i]
strings = self.tickStrings(values, self.scale, spacing)
strings = self.tickStrings(values, self.autoSIPrefixScale * self.scale, spacing)
else:
strings = tickStrings[i]
@ -683,18 +792,41 @@ class AxisItem(GraphicsWidget):
if tickPositions[i][j] is None:
strings[j] = None
textRects.extend([p.boundingRect(QtCore.QRectF(0, 0, 100, 100), QtCore.Qt.AlignCenter, str(s)) for s in strings if s is not None])
## Measure density of text; decide whether to draw this level
rects = []
for s in strings:
if s is None:
rects.append(None)
else:
br = p.boundingRect(QtCore.QRectF(0, 0, 100, 100), QtCore.Qt.AlignCenter, str(s))
## boundingRect is usually just a bit too large
## (but this probably depends on per-font metrics?)
br.setHeight(br.height() * 0.8)
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])
else:
textSize = np.sum([r.width() for r in textRects])
textSize2 = np.max([r.height() for r in textRects])
## If the strings are too crowded, stop drawing text now
## If the strings are too crowded, stop drawing text now.
## We use three different crowding limits based on the number
## of texts drawn so far.
textFillRatio = float(textSize) / lengthInPixels
if textFillRatio > 0.7:
finished = False
for nTexts, limit in self.style['textFillLimits']:
if len(textSpecs) >= nTexts and textFillRatio >= limit:
finished = True
break
if finished:
break
#spacing, values = tickLevels[best]
#strings = self.tickStrings(values, self.scale, spacing)
for j in range(len(strings)):
@ -703,24 +835,61 @@ class AxisItem(GraphicsWidget):
continue
vstr = str(vstr)
x = tickPositions[i][j]
textRect = p.boundingRect(QtCore.QRectF(0, 0, 100, 100), QtCore.Qt.AlignCenter, vstr)
#textRect = p.boundingRect(QtCore.QRectF(0, 0, 100, 100), QtCore.Qt.AlignCenter, vstr)
textRect = rects[j]
height = textRect.height()
self.textHeight = height
width = textRect.width()
#self.textHeight = height
offset = max(0,self.tickLength) + textOffset
if self.orientation == 'left':
textFlags = QtCore.Qt.AlignRight|QtCore.Qt.AlignVCenter
rect = QtCore.QRectF(tickStop-100, x-(height/2), 99-max(0,self.tickLength), height)
textFlags = QtCore.Qt.TextDontClip|QtCore.Qt.AlignRight|QtCore.Qt.AlignVCenter
rect = QtCore.QRectF(tickStop-offset-width, x-(height/2), width, height)
elif self.orientation == 'right':
textFlags = QtCore.Qt.AlignLeft|QtCore.Qt.AlignVCenter
rect = QtCore.QRectF(tickStop+max(0,self.tickLength)+1, x-(height/2), 100-max(0,self.tickLength), height)
textFlags = QtCore.Qt.TextDontClip|QtCore.Qt.AlignLeft|QtCore.Qt.AlignVCenter
rect = QtCore.QRectF(tickStop+offset, x-(height/2), width, height)
elif self.orientation == 'top':
textFlags = QtCore.Qt.AlignCenter|QtCore.Qt.AlignBottom
rect = QtCore.QRectF(x-100, tickStop-max(0,self.tickLength)-height, 200, height)
textFlags = QtCore.Qt.TextDontClip|QtCore.Qt.AlignCenter|QtCore.Qt.AlignBottom
rect = QtCore.QRectF(x-width/2., tickStop-offset-height, width, height)
elif self.orientation == 'bottom':
textFlags = QtCore.Qt.AlignCenter|QtCore.Qt.AlignTop
rect = QtCore.QRectF(x-100, tickStop+max(0,self.tickLength), 200, height)
textFlags = QtCore.Qt.TextDontClip|QtCore.Qt.AlignCenter|QtCore.Qt.AlignTop
rect = QtCore.QRectF(x-width/2., tickStop+offset, width, height)
p.setPen(self.pen())
p.drawText(rect, textFlags, vstr)
#p.setPen(self.pen())
#p.drawText(rect, textFlags, vstr)
textSpecs.append((rect, textFlags, vstr))
prof.mark('compute text')
## update max text size if needed.
self._updateMaxTextSize(textSize2)
return (axisSpec, tickSpecs, textSpecs)
def drawPicture(self, p, axisSpec, tickSpecs, textSpecs):
prof = debug.Profiler("AxisItem.drawPicture", disabled=True)
p.setRenderHint(p.Antialiasing, False)
p.setRenderHint(p.TextAntialiasing, True)
## draw long line along axis
pen, p1, p2 = axisSpec
p.setPen(pen)
p.drawLine(p1, p2)
p.translate(0.5,0) ## resolves some damn pixel ambiguity
## draw ticks
for pen, p1, p2 in tickSpecs:
p.setPen(pen)
p.drawLine(p1, p2)
prof.mark('draw ticks')
## Draw all text
if self.tickFont is not None:
p.setFont(self.tickFont)
p.setPen(self.pen())
for rect, flags, text in textSpecs:
p.drawText(rect, flags, text)
#p.drawRect(rect)
prof.mark('draw text')
prof.finish()

View File

@ -0,0 +1,149 @@
import pyqtgraph as pg
from pyqtgraph.Qt import QtGui, QtCore
from .GraphicsObject import GraphicsObject
import numpy as np
__all__ = ['BarGraphItem']
class BarGraphItem(GraphicsObject):
def __init__(self, **opts):
"""
Valid keyword options are:
x, x0, x1, y, y0, y1, width, height, pen, brush
x specifies the x-position of the center of the bar.
x0, x1 specify left and right edges of the bar, respectively.
width specifies distance from x0 to x1.
You may specify any combination:
x, width
x0, width
x1, width
x0, x1
Likewise y, y0, y1, and height.
If only height is specified, then y0 will be set to 0
Example uses:
BarGraphItem(x=range(5), height=[1,5,2,4,3], width=0.5)
"""
GraphicsObject.__init__(self)
self.opts = dict(
x=None,
y=None,
x0=None,
y0=None,
x1=None,
y1=None,
height=None,
width=None,
pen=None,
brush=None,
pens=None,
brushes=None,
)
self.setOpts(**opts)
def setOpts(self, **opts):
self.opts.update(opts)
self.picture = None
self.update()
self.informViewBoundsChanged()
def drawPicture(self):
self.picture = QtGui.QPicture()
p = QtGui.QPainter(self.picture)
pen = self.opts['pen']
pens = self.opts['pens']
if pen is None and pens is None:
pen = pg.getConfigOption('foreground')
brush = self.opts['brush']
brushes = self.opts['brushes']
if brush is None and brushes is None:
brush = (128, 128, 128)
def asarray(x):
if x is None or np.isscalar(x) or isinstance(x, np.ndarray):
return x
return np.array(x)
x = asarray(self.opts.get('x'))
x0 = asarray(self.opts.get('x0'))
x1 = asarray(self.opts.get('x1'))
width = asarray(self.opts.get('width'))
if x0 is None:
if width is None:
raise Exception('must specify either x0 or width')
if x1 is not None:
x0 = x1 - width
elif x is not None:
x0 = x - width/2.
else:
raise Exception('must specify at least one of x, x0, or x1')
if width is None:
if x1 is None:
raise Exception('must specify either x1 or width')
width = x1 - x0
y = asarray(self.opts.get('y'))
y0 = asarray(self.opts.get('y0'))
y1 = asarray(self.opts.get('y1'))
height = asarray(self.opts.get('height'))
if y0 is None:
if height is None:
y0 = 0
elif y1 is not None:
y0 = y1 - height
elif y is not None:
y0 = y - height/2.
else:
y0 = 0
if height is None:
if y1 is None:
raise Exception('must specify either y1 or height')
height = y1 - y0
p.setPen(pg.mkPen(pen))
p.setBrush(pg.mkBrush(brush))
for i in range(len(x0)):
if pens is not None:
p.setPen(pg.mkPen(pens[i]))
if brushes is not None:
p.setBrush(pg.mkBrush(brushes[i]))
if np.isscalar(y0):
y = y0
else:
y = y0[i]
if np.isscalar(width):
w = width
else:
w = width[i]
p.drawRect(QtCore.QRectF(x0[i], y, w, height[i]))
p.end()
self.prepareGeometryChange()
def paint(self, p, *args):
if self.picture is None:
self.drawPicture()
self.picture.play(p)
def boundingRect(self):
if self.picture is None:
self.drawPicture()
return QtCore.QRectF(self.picture.boundingRect())

View File

@ -103,11 +103,18 @@ class GraphItem(GraphicsObject):
def paint(self, p, *args):
if self.picture == None:
self.generatePicture()
if pg.getConfigOption('antialias') is True:
p.setRenderHint(p.Antialiasing)
self.picture.play(p)
def boundingRect(self):
return self.scatter.boundingRect()
def dataBounds(self, *args, **kwds):
return self.scatter.dataBounds(*args, **kwds)
def pixelPadding(self):
return self.scatter.pixelPadding()

View File

@ -446,6 +446,14 @@ class GraphicsItem(object):
#print " --> ", ch2.scene()
#self.setChildScene(ch2)
def parentChanged(self):
"""Called when the item's parent has changed.
This method handles connecting / disconnecting from ViewBox signals
to make sure viewRangeChanged works properly. It should generally be
extended, not overridden."""
self._updateView()
def _updateView(self):
## called to see whether this item has a new view to connect to
## NOTE: This is called from GraphicsObject.itemChange or GraphicsWidget.itemChange.
@ -496,6 +504,12 @@ class GraphicsItem(object):
## inform children that their view might have changed
self._replaceView(oldView)
self.viewChanged(view, oldView)
def viewChanged(self, view, oldView):
"""Called when this item's view has changed
(ie, the item has been added to or removed from a ViewBox)"""
pass
def _replaceView(self, oldView, item=None):
if item is None:
@ -519,6 +533,7 @@ class GraphicsItem(object):
def viewTransformChanged(self):
"""
Called whenever the transformation matrix of the view has changed.
(eg, the view range has changed or the view was resized)
"""
pass

View File

@ -12,6 +12,7 @@ class GraphicsObject(GraphicsItem, QtGui.QGraphicsObject):
"""
_qtBaseClass = QtGui.QGraphicsObject
def __init__(self, *args):
self.__inform_view_on_changes = True
QtGui.QGraphicsObject.__init__(self, *args)
self.setFlag(self.ItemSendsGeometryChanges)
GraphicsItem.__init__(self)
@ -19,8 +20,8 @@ class GraphicsObject(GraphicsItem, QtGui.QGraphicsObject):
def itemChange(self, change, value):
ret = QtGui.QGraphicsObject.itemChange(self, change, value)
if change in [self.ItemParentHasChanged, self.ItemSceneHasChanged]:
self._updateView()
if change in [self.ItemPositionHasChanged, self.ItemTransformHasChanged]:
self.parentChanged()
if self.__inform_view_on_changes and change in [self.ItemPositionHasChanged, self.ItemTransformHasChanged]:
self.informViewBoundsChanged()
## workaround for pyqt bug:

View File

@ -20,16 +20,17 @@ class GraphicsWidget(GraphicsItem, QtGui.QGraphicsWidget):
## done by GraphicsItem init
#GraphicsScene.registerObject(self) ## workaround for pyqt bug in graphicsscene.items()
## Removed because this causes segmentation faults. Don't know why.
# def itemChange(self, change, value):
# ret = QtGui.QGraphicsWidget.itemChange(self, change, value) ## segv occurs here
# if change in [self.ItemParentHasChanged, self.ItemSceneHasChanged]:
# self._updateView()
# return ret
# Removed due to https://bugreports.qt-project.org/browse/PYSIDE-86
#def itemChange(self, change, value):
## BEWARE: Calling QGraphicsWidget.itemChange can lead to crashing!
##ret = QtGui.QGraphicsWidget.itemChange(self, change, value) ## segv occurs here
## The default behavior is just to return the value argument, so we'll do that
## without calling the original method.
#ret = value
#if change in [self.ItemParentHasChanged, self.ItemSceneHasChanged]:
#self._updateView()
#return ret
#def getMenu(self):
#pass
def setFixedHeight(self, h):
self.setMaximumHeight(h)
self.setMinimumHeight(h)

View File

@ -5,7 +5,9 @@ from ..Point import Point
class GraphicsWidgetAnchor(object):
"""
Class used to allow GraphicsWidgets to anchor to a specific position on their
parent.
parent. The item will be automatically repositioned if the parent is resized.
This is used, for example, to anchor a LegendItem to a corner of its parent
PlotItem.
"""
@ -45,7 +47,52 @@ class GraphicsWidgetAnchor(object):
self.__parentAnchor = parentPos
self.__offset = offset
self.__geometryChanged()
def autoAnchor(self, pos, relative=True):
"""
Set the position of this item relative to its parent by automatically
choosing appropriate anchor settings.
If relative is True, one corner of the item will be anchored to
the appropriate location on the parent with no offset. The anchored
corner will be whichever is closest to the parent's boundary.
If relative is False, one corner of the item will be anchored to the same
corner of the parent, with an absolute offset to achieve the correct
position.
"""
pos = Point(pos)
br = self.mapRectToParent(self.boundingRect()).translated(pos - self.pos())
pbr = self.parentItem().boundingRect()
anchorPos = [0,0]
parentPos = Point()
itemPos = Point()
if abs(br.left() - pbr.left()) < abs(br.right() - pbr.right()):
anchorPos[0] = 0
parentPos[0] = pbr.left()
itemPos[0] = br.left()
else:
anchorPos[0] = 1
parentPos[0] = pbr.right()
itemPos[0] = br.right()
if abs(br.top() - pbr.top()) < abs(br.bottom() - pbr.bottom()):
anchorPos[1] = 0
parentPos[1] = pbr.top()
itemPos[1] = br.top()
else:
anchorPos[1] = 1
parentPos[1] = pbr.bottom()
itemPos[1] = br.bottom()
if relative:
relPos = [(itemPos[0]-pbr.left()) / pbr.width(), (itemPos[1]-pbr.top()) / pbr.height()]
self.anchor(anchorPos, relPos)
else:
offset = itemPos - parentPos
self.anchor(anchorPos, anchorPos, offset)
def __geometryChanged(self):
if self.__parent is None:
return

View File

@ -196,10 +196,12 @@ class ImageItem(GraphicsObject):
return
else:
gotNewData = True
if self.image is None or image.shape != self.image.shape:
self.prepareGeometryChange()
shapeChanged = (self.image is None or image.shape != self.image.shape)
self.image = image.view(np.ndarray)
if shapeChanged:
self.prepareGeometryChange()
self.informViewBoundsChanged()
prof.mark('1')
if autoLevels is None:
@ -249,7 +251,7 @@ class ImageItem(GraphicsObject):
def render(self):
prof = debug.Profiler('ImageItem.render', disabled=True)
if self.image is None:
if self.image is None or self.image.size == 0:
return
if isinstance(self.lut, collections.Callable):
lut = self.lut(self.image)
@ -269,6 +271,8 @@ class ImageItem(GraphicsObject):
return
if self.qimage is None:
self.render()
if self.qimage is None:
return
prof.mark('render QImage')
if self.paintMode is not None:
p.setCompositionMode(self.paintMode)

View File

@ -2,11 +2,12 @@ from pyqtgraph.Qt import QtGui, QtCore
import pyqtgraph.functions as fn
import pyqtgraph as pg
from .GraphicsWidget import GraphicsWidget
from .GraphicsWidgetAnchor import GraphicsWidgetAnchor
__all__ = ['LabelItem']
class LabelItem(GraphicsWidget):
class LabelItem(GraphicsWidget, GraphicsWidgetAnchor):
"""
GraphicsWidget displaying text.
Used mainly as axis labels, titles, etc.
@ -17,6 +18,7 @@ class LabelItem(GraphicsWidget):
def __init__(self, text=' ', parent=None, angle=0, **args):
GraphicsWidget.__init__(self, parent)
GraphicsWidgetAnchor.__init__(self)
self.item = QtGui.QGraphicsTextItem(self)
self.opts = {
'color': None,

View File

@ -4,6 +4,7 @@ from ..Qt import QtGui, QtCore
from .. import functions as fn
from ..Point import Point
from .GraphicsWidgetAnchor import GraphicsWidgetAnchor
import pyqtgraph as pg
__all__ = ['LegendItem']
class LegendItem(GraphicsWidget, GraphicsWidgetAnchor):
@ -62,18 +63,43 @@ class LegendItem(GraphicsWidget, GraphicsWidgetAnchor):
=========== ========================================================
Arguments
item A PlotDataItem from which the line and point style
of the item will be determined
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)
sample = ItemSample(item)
if isinstance(item, ItemSample):
sample = item
else:
sample = ItemSample(item)
row = len(self.items)
self.items.append((sample, label))
self.layout.addItem(sample, row, 0)
self.layout.addItem(label, row, 1)
self.updateSize()
def removeItem(self, name):
"""
Removes one item from the legend.
=========== ========================================================
Arguments
title The title displayed for this item.
=========== ========================================================
"""
# Thanks, Ulrich!
# cycle for a match
for sample, label in self.items:
if label.text == name: # hit
self.items.remove( (sample, label) ) # remove from itemlist
self.layout.removeItem(sample) # remove from layout
sample.close() # remove from drawing
self.layout.removeItem(label)
label.close()
self.updateSize() # redraq box
def updateSize(self):
if self.size is not None:
return
@ -87,17 +113,29 @@ class LegendItem(GraphicsWidget, GraphicsWidgetAnchor):
#print(width, height)
#print width, height
self.setGeometry(0, 0, width+25, height)
def boundingRect(self):
return QtCore.QRectF(0, 0, self.width(), self.height())
def paint(self, p, *args):
p.setPen(fn.mkPen(255,255,255,100))
p.setBrush(fn.mkBrush(100,100,100,50))
p.drawRect(self.boundingRect())
def hoverEvent(self, ev):
ev.acceptDrags(QtCore.Qt.LeftButton)
def mouseDragEvent(self, ev):
if ev.button() == QtCore.Qt.LeftButton:
dpos = ev.pos() - ev.lastPos()
self.autoAnchor(self.pos() + dpos)
class ItemSample(GraphicsWidget):
""" Class responsible for drawing a single item in a LegendItem (sans label).
This may be subclassed to draw custom graphics in a Legend.
"""
## Todo: make this more generic; let each item decide how it should be represented.
def __init__(self, item):
GraphicsWidget.__init__(self)
self.item = item
@ -106,6 +144,7 @@ class ItemSample(GraphicsWidget):
return QtCore.QRectF(0, 0, 20, 20)
def paint(self, p, *args):
#p.setRenderHint(p.Antialiasing) # only if the data is antialiased.
opts = self.item.opts
if opts.get('fillLevel',None) is not None and opts.get('fillBrush',None) is not None:
@ -113,8 +152,21 @@ class ItemSample(GraphicsWidget):
p.setPen(fn.mkPen(None))
p.drawPolygon(QtGui.QPolygonF([QtCore.QPointF(2,18), QtCore.QPointF(18,2), QtCore.QPointF(18,18)]))
p.setPen(fn.mkPen(opts['pen']))
p.drawLine(2, 18, 18, 2)
if not isinstance(self.item, pg.ScatterPlotItem):
p.setPen(fn.mkPen(opts['pen']))
p.drawLine(2, 18, 18, 2)
symbol = opts.get('symbol', None)
if symbol is not None:
if isinstance(self.item, pg.PlotDataItem):
opts = self.item.scatter.opts
pen = pg.mkPen(opts['pen'])
brush = pg.mkBrush(opts['brush'])
size = opts['size']
p.translate(10,10)
path = pg.graphicsItems.ScatterPlotItem.drawSymbol(p, symbol, size, pen, brush)

View File

@ -1,7 +1,11 @@
from pyqtgraph.Qt import QtGui, QtCore
from scipy.fftpack import fft
try:
from pyqtgraph.Qt import QtOpenGL
HAVE_OPENGL = True
except:
HAVE_OPENGL = False
import numpy as np
import scipy.stats
from .GraphicsObject import GraphicsObject
import pyqtgraph.functions as fn
from pyqtgraph import debug
@ -21,7 +25,6 @@ class PlotCurveItem(GraphicsObject):
Features:
- Fast data update
- FFT display mode (accessed via PlotItem context menu)
- Fill under curve
- Mouse interaction
@ -65,7 +68,8 @@ class PlotCurveItem(GraphicsObject):
'brush': None,
'stepMode': False,
'name': None,
'antialias': pg.getConfigOption('antialias'),
'antialias': pg.getConfigOption('antialias'),\
'connect': 'all',
}
self.setClickable(kargs.get('clickable', False))
self.setData(*args, **kargs)
@ -106,16 +110,21 @@ class PlotCurveItem(GraphicsObject):
if orthoRange is not None:
mask = (d2 >= orthoRange[0]) * (d2 <= orthoRange[1])
d = d[mask]
d2 = d2[mask]
#d2 = d2[mask]
if len(d) == 0:
return (None, None)
## Get min/max (or percentiles) of the requested data range
if frac >= 1.0:
b = (d.min(), d.max())
b = (np.nanmin(d), np.nanmax(d))
elif frac <= 0.0:
raise Exception("Value for parameter 'frac' must be > 0. (got %s)" % str(frac))
else:
b = (scipy.stats.scoreatpercentile(d, 50 - (frac * 50)), scipy.stats.scoreatpercentile(d, 50 + (frac * 50)))
mask = np.isfinite(d)
d = d[mask]
b = np.percentile(d, [50 * (1 - frac), 50 * (1 + frac)])
## adjust for fill level
if ax == 1 and self.opts['fillLevel'] is not None:
b = (min(b[0], self.opts['fillLevel']), max(b[1], self.opts['fillLevel']))
@ -252,6 +261,15 @@ class PlotCurveItem(GraphicsObject):
by :func:`mkBrush <pyqtgraph.mkBrush>` is allowed.
antialias (bool) Whether to use antialiasing when drawing. This
is disabled by default because it decreases performance.
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
connect Argument specifying how vertexes should be connected
by line segments. Default is "all", indicating full
connection. "pairs" causes only even-numbered segments
to be drawn. "finite" causes segments to be omitted if
they are attached to nan or inf values. For any other
connectivity, specify an array of boolean values.
============== ========================================================
If non-keyword arguments are used, they will be interpreted as
@ -303,10 +321,10 @@ class PlotCurveItem(GraphicsObject):
if self.opts['stepMode'] is True:
if len(self.xData) != len(self.yData)+1: ## allow difference of 1 for step mode plots
raise Exception("len(X) must be len(Y)+1 since stepMode=True (got %s and %s)" % (str(x.shape), str(y.shape)))
raise Exception("len(X) must be len(Y)+1 since stepMode=True (got %s and %s)" % (self.xData.shape, self.yData.shape))
else:
if self.xData.shape != self.yData.shape: ## allow difference of 1 for step mode plots
raise Exception("X and Y arrays must be the same shape--got %s and %s." % (str(x.shape), str(y.shape)))
raise Exception("X and Y arrays must be the same shape--got %s and %s." % (self.xData.shape, self.yData.shape))
self.path = None
self.fillPath = None
@ -314,7 +332,8 @@ class PlotCurveItem(GraphicsObject):
if 'name' in kargs:
self.opts['name'] = kargs['name']
if 'connect' in kargs:
self.opts['connect'] = kargs['connect']
if 'pen' in kargs:
self.setPen(kargs['pen'])
if 'shadowPen' in kargs:
@ -353,7 +372,7 @@ class PlotCurveItem(GraphicsObject):
y[0] = self.opts['fillLevel']
y[-1] = self.opts['fillLevel']
path = fn.arrayToQPath(x, y, connect='all')
path = fn.arrayToQPath(x, y, connect=self.opts['connect'])
return path
@ -366,16 +385,16 @@ class PlotCurveItem(GraphicsObject):
return QtGui.QPainterPath()
return self.path
@pg.debug.warnOnException ## raising an exception here causes crash
def paint(self, p, opt, widget):
prof = debug.Profiler('PlotCurveItem.paint '+str(id(self)), disabled=True)
if self.xData is None:
return
#if self.opts['spectrumMode']:
#if self.specPath is None:
#self.specPath = self.generatePath(*self.getData())
#path = self.specPath
#else:
if HAVE_OPENGL and pg.getConfigOption('enableExperimental') and isinstance(widget, QtOpenGL.QGLWidget):
self.paintGL(p, opt, widget)
return
x = None
y = None
if self.path is None:
@ -385,7 +404,6 @@ class PlotCurveItem(GraphicsObject):
self.path = self.generatePath(x,y)
self.fillPath = None
path = self.path
prof.mark('generate path')
@ -440,6 +458,65 @@ class PlotCurveItem(GraphicsObject):
#p.setPen(QtGui.QPen(QtGui.QColor(255,0,0)))
#p.drawRect(self.boundingRect())
def paintGL(self, p, opt, widget):
p.beginNativePainting()
import OpenGL.GL as gl
## set clipping viewport
view = self.getViewBox()
if view is not None:
rect = view.mapRectToItem(self, view.boundingRect())
#gl.glViewport(int(rect.x()), int(rect.y()), int(rect.width()), int(rect.height()))
#gl.glTranslate(-rect.x(), -rect.y(), 0)
gl.glEnable(gl.GL_STENCIL_TEST)
gl.glColorMask(gl.GL_FALSE, gl.GL_FALSE, gl.GL_FALSE, gl.GL_FALSE) # disable drawing to frame buffer
gl.glDepthMask(gl.GL_FALSE) # disable drawing to depth buffer
gl.glStencilFunc(gl.GL_NEVER, 1, 0xFF)
gl.glStencilOp(gl.GL_REPLACE, gl.GL_KEEP, gl.GL_KEEP)
## draw stencil pattern
gl.glStencilMask(0xFF);
gl.glClear(gl.GL_STENCIL_BUFFER_BIT)
gl.glBegin(gl.GL_TRIANGLES)
gl.glVertex2f(rect.x(), rect.y())
gl.glVertex2f(rect.x()+rect.width(), rect.y())
gl.glVertex2f(rect.x(), rect.y()+rect.height())
gl.glVertex2f(rect.x()+rect.width(), rect.y()+rect.height())
gl.glVertex2f(rect.x()+rect.width(), rect.y())
gl.glVertex2f(rect.x(), rect.y()+rect.height())
gl.glEnd()
gl.glColorMask(gl.GL_TRUE, gl.GL_TRUE, gl.GL_TRUE, gl.GL_TRUE)
gl.glDepthMask(gl.GL_TRUE)
gl.glStencilMask(0x00)
gl.glStencilFunc(gl.GL_EQUAL, 1, 0xFF)
try:
x, y = self.getData()
pos = np.empty((len(x), 2))
pos[:,0] = x
pos[:,1] = y
gl.glEnableClientState(gl.GL_VERTEX_ARRAY)
try:
gl.glVertexPointerf(pos)
pen = fn.mkPen(self.opts['pen'])
color = pen.color()
gl.glColor4f(color.red()/255., color.green()/255., color.blue()/255., color.alpha()/255.)
width = pen.width()
if pen.isCosmetic() and width < 1:
width = 1
gl.glPointSize(width)
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.glDrawArrays(gl.GL_LINE_STRIP, 0, pos.size / pos.shape[-1])
finally:
gl.glDisableClientState(gl.GL_VERTEX_ARRAY)
finally:
p.endNativePainting()
def clear(self):
self.xData = None ## raw values

View File

@ -4,7 +4,6 @@ from .GraphicsObject import GraphicsObject
from .PlotCurveItem import PlotCurveItem
from .ScatterPlotItem import ScatterPlotItem
import numpy as np
import scipy
import pyqtgraph.functions as fn
import pyqtgraph.debug as debug
import pyqtgraph as pg
@ -58,6 +57,8 @@ class PlotDataItem(GraphicsObject):
**Line style keyword arguments:**
========== ================================================
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>`
@ -84,13 +85,28 @@ class PlotDataItem(GraphicsObject):
**Optimization keyword arguments:**
========== =====================================================================
antialias (bool) By default, antialiasing is disabled to improve performance.
Note that in some cases (in particluar, when pxMode=True), points
will be rendered antialiased even if this is set to False.
identical *deprecated*
decimate (int) sub-sample data by selecting every nth sample before plotting
========== =====================================================================
================ =====================================================================
antialias (bool) By default, antialiasing is disabled to improve performance.
Note that in some cases (in particluar, when pxMode=True), points
will be rendered antialiased even if this is set to False.
decimate deprecated.
downsample (int) Reduce the number of samples displayed by this value
downsampleMethod 'subsample': Downsample by taking the first of N samples.
This method is fastest and least accurate.
'mean': Downsample by taking the mean of N samples.
'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.
autoDownsample (bool) If True, resample the data before plotting to avoid plotting
multiple line segments per pixel. This can improve performance when
viewing very high-density data, but increases the initial overhead
and memory usage.
clipToView (bool) If True, only plot data that is visible within the X range of
the containing ViewBox. This can improve performance when plotting
very large data sets where only a fraction of the data is visible
at any time.
identical *deprecated*
================ =====================================================================
**Meta-info keyword arguments:**
@ -104,7 +120,7 @@ class PlotDataItem(GraphicsObject):
self.yData = None
self.xDisp = None
self.yDisp = None
self.dataMask = None
#self.dataMask = None
#self.curves = []
#self.scatters = []
self.curve = PlotCurveItem()
@ -118,9 +134,10 @@ class PlotDataItem(GraphicsObject):
#self.clear()
self.opts = {
'connect': 'all',
'fftMode': False,
'logMode': [False, False],
'downsample': False,
'alphaHint': 1.0,
'alphaMode': False,
@ -138,6 +155,11 @@ class PlotDataItem(GraphicsObject):
'antialias': pg.getConfigOption('antialias'),
'pointMode': None,
'downsample': 1,
'autoDownsample': False,
'downsampleMethod': 'peak',
'clipToView': False,
'data': None,
}
self.setData(*args, **kargs)
@ -164,6 +186,7 @@ class PlotDataItem(GraphicsObject):
return
self.opts['fftMode'] = mode
self.xDisp = self.yDisp = None
self.xClean = self.yClean = None
self.updateItems()
self.informViewBoundsChanged()
@ -172,6 +195,7 @@ class PlotDataItem(GraphicsObject):
return
self.opts['logMode'] = [xMode, yMode]
self.xDisp = self.yDisp = None
self.xClean = self.yClean = None
self.updateItems()
self.informViewBoundsChanged()
@ -258,13 +282,51 @@ class PlotDataItem(GraphicsObject):
#self.scatter.setSymbolSize(symbolSize)
self.updateItems()
def setDownsampling(self, ds):
if self.opts['downsample'] == ds:
def setDownsampling(self, ds=None, auto=None, method=None):
"""
Set the downsampling mode of this item. Downsampling reduces the number
of samples drawn to increase performance.
=========== =================================================================
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
mode 'subsample': Downsample by taking the first of N samples.
This method is fastest and least accurate.
'mean': Downsample by taking the mean of N samples.
'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:
if self.opts['downsample'] != ds:
changed = True
self.opts['downsample'] = ds
if auto is not None and self.opts['autoDownsample'] != auto:
self.opts['autoDownsample'] = auto
changed = True
if method is not None:
if self.opts['downsampleMethod'] != method:
changed = True
self.opts['downsampleMethod'] = method
if changed:
self.xDisp = self.yDisp = None
self.updateItems()
def setClipToView(self, clip):
if self.opts['clipToView'] == clip:
return
self.opts['downsample'] = ds
self.opts['clipToView'] = clip
self.xDisp = self.yDisp = None
self.updateItems()
def setData(self, *args, **kargs):
"""
Clear any data displayed by this item and display new data.
@ -304,7 +366,7 @@ class PlotDataItem(GraphicsObject):
raise Exception('Invalid data type %s' % type(data))
elif len(args) == 2:
seq = ('listOfValues', 'MetaArray')
seq = ('listOfValues', 'MetaArray', 'empty')
if dataType(args[0]) not in seq or dataType(args[1]) not in seq:
raise Exception('When passing two unnamed arguments, both must be a list or array of values. (got %s, %s)' % (str(type(args[0])), str(type(args[1]))))
if not isinstance(args[0], np.ndarray):
@ -327,6 +389,8 @@ class PlotDataItem(GraphicsObject):
if 'name' in kargs:
self.opts['name'] = kargs['name']
if 'connect' in kargs:
self.opts['connect'] = kargs['connect']
## if symbol pen/brush are given with no symbol, then assume symbol is 'o'
@ -365,6 +429,7 @@ class PlotDataItem(GraphicsObject):
self.xData = x.view(np.ndarray) ## one last check to make sure there are no MetaArrays getting by
self.yData = y.view(np.ndarray)
self.xClean = self.yClean = None
self.xDisp = None
self.yDisp = None
prof.mark('set data')
@ -385,7 +450,7 @@ class PlotDataItem(GraphicsObject):
def updateItems(self):
curveArgs = {}
for k,v in [('pen','pen'), ('shadowPen','shadowPen'), ('fillLevel','fillLevel'), ('fillBrush', 'brush'), ('antialias', 'antialias')]:
for k,v in [('pen','pen'), ('shadowPen','shadowPen'), ('fillLevel','fillLevel'), ('fillBrush', 'brush'), ('antialias', 'antialias'), ('connect', 'connect')]:
curveArgs[v] = self.opts[k]
scatterArgs = {}
@ -394,7 +459,7 @@ class PlotDataItem(GraphicsObject):
scatterArgs[v] = self.opts[k]
x,y = self.getData()
scatterArgs['mask'] = self.dataMask
#scatterArgs['mask'] = self.dataMask
if curveArgs['pen'] is not None or (curveArgs['brush'] is not None and curveArgs['fillLevel'] is not None):
self.curve.setData(x=x, y=y, **curveArgs)
@ -412,40 +477,89 @@ class PlotDataItem(GraphicsObject):
def getData(self):
if self.xData is None:
return (None, None)
if self.xDisp is None:
nanMask = np.isnan(self.xData) | np.isnan(self.yData) | np.isinf(self.xData) | np.isinf(self.yData)
if any(nanMask):
self.dataMask = ~nanMask
x = self.xData[self.dataMask]
y = self.yData[self.dataMask]
else:
self.dataMask = None
x = self.xData
y = self.yData
#if self.xClean is None:
#nanMask = np.isnan(self.xData) | np.isnan(self.yData) | np.isinf(self.xData) | np.isinf(self.yData)
#if nanMask.any():
#self.dataMask = ~nanMask
#self.xClean = self.xData[self.dataMask]
#self.yClean = self.yData[self.dataMask]
#else:
#self.dataMask = None
#self.xClean = self.xData
#self.yClean = self.yData
ds = self.opts['downsample']
if ds > 1:
x = x[::ds]
#y = resample(y[:len(x)*ds], len(x)) ## scipy.signal.resample causes nasty ringing
y = y[::ds]
if self.xDisp is None:
x = self.xData
y = self.yData
#ds = self.opts['downsample']
#if isinstance(ds, int) and ds > 1:
#x = x[::ds]
##y = resample(y[:len(x)*ds], len(x)) ## scipy.signal.resample causes nasty ringing
#y = y[::ds]
if self.opts['fftMode']:
f = np.fft.fft(y) / len(y)
y = abs(f[1:len(f)/2])
dt = x[-1] - x[0]
x = np.linspace(0, 0.5*len(x)/dt, len(y))
x,y = self._fourierTransform(x, y)
if self.opts['logMode'][0]:
x = np.log10(x)
if self.opts['logMode'][1]:
y = np.log10(y)
if any(self.opts['logMode']): ## re-check for NANs after log
nanMask = np.isinf(x) | np.isinf(y) | np.isnan(x) | np.isnan(y)
if any(nanMask):
self.dataMask = ~nanMask
x = x[self.dataMask]
y = y[self.dataMask]
else:
self.dataMask = None
#if any(self.opts['logMode']): ## re-check for NANs after log
#nanMask = np.isinf(x) | np.isinf(y) | np.isnan(x) | np.isnan(y)
#if any(nanMask):
#self.dataMask = ~nanMask
#x = x[self.dataMask]
#y = y[self.dataMask]
#else:
#self.dataMask = None
ds = self.opts['downsample']
if not isinstance(ds, int):
ds = 1
if self.opts['autoDownsample']:
# this option presumes that x-values have uniform spacing
range = self.viewRect()
if range is not None:
dx = float(x[-1]-x[0]) / (len(x)-1)
x0 = (range.left()-x[0]) / dx
x1 = (range.right()-x[0]) / dx
width = self.getViewBox().width()
ds = int(max(1, int(0.2 * (x1-x0) / width)))
## downsampling is expensive; delay until after clipping.
if self.opts['clipToView']:
# this option presumes that x-values have uniform spacing
range = self.viewRect()
if range is not None:
dx = float(x[-1]-x[0]) / (len(x)-1)
# clip to visible region extended by downsampling value
x0 = np.clip(int((range.left()-x[0])/dx)-1*ds , 0, len(x)-1)
x1 = np.clip(int((range.right()-x[0])/dx)+2*ds , 0, len(x)-1)
x = x[x0:x1]
y = y[x0:x1]
if ds > 1:
if self.opts['downsampleMethod'] == 'subsample':
x = x[::ds]
y = y[::ds]
elif self.opts['downsampleMethod'] == 'mean':
n = len(x) / ds
x = x[:n*ds:ds]
y = y[:n*ds].reshape(n,ds).mean(axis=1)
elif self.opts['downsampleMethod'] == 'peak':
n = len(x) / ds
x1 = np.empty((n,2))
x1[:] = x[:n*ds:ds,np.newaxis]
x = x1.reshape(n*2)
y1 = np.empty((n,2))
y2 = y[:n*ds].reshape((n, ds))
y1[:,0] = y2.max(axis=1)
y1[:,1] = y2.min(axis=1)
y = y1.reshape(n*2)
self.xDisp = x
self.yDisp = y
#print self.yDisp.shape, self.yDisp.min(), self.yDisp.max()
@ -482,33 +596,6 @@ class PlotDataItem(GraphicsObject):
r2[1] if range[1] is None else (range[1] if r2[1] is None else min(r2[1], range[1]))
]
return range
#if frac <= 0.0:
#raise Exception("Value for parameter 'frac' must be > 0. (got %s)" % str(frac))
#(x, y) = self.getData()
#if x is None or len(x) == 0:
#return None
#if ax == 0:
#d = x
#d2 = y
#elif ax == 1:
#d = y
#d2 = x
#if orthoRange is not None:
#mask = (d2 >= orthoRange[0]) * (d2 <= orthoRange[1])
#d = d[mask]
##d2 = d2[mask]
#if len(d) > 0:
#if frac >= 1.0:
#return (np.min(d), np.max(d))
#else:
#return (scipy.stats.scoreatpercentile(d, 50 - (frac * 50)), scipy.stats.scoreatpercentile(d, 50 + (frac * 50)))
#else:
#return None
def pixelPadding(self):
"""
@ -531,6 +618,8 @@ class PlotDataItem(GraphicsObject):
#self.scatters = []
self.xData = None
self.yData = None
#self.xClean = None
#self.yClean = None
self.xDisp = None
self.yDisp = None
self.curve.setData([])
@ -546,6 +635,27 @@ class PlotDataItem(GraphicsObject):
self.sigClicked.emit(self)
self.sigPointsClicked.emit(self, points)
def viewRangeChanged(self):
# view range has changed; re-plot if needed
if self.opts['clipToView'] or self.opts['autoDownsample']:
self.xDisp = self.yDisp = None
self.updateItems()
def _fourierTransform(self, x, y):
## Perform fourier transform. If x values are not sampled uniformly,
## then use interpolate.griddata 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')
x = x2
f = np.fft.fft(y) / len(y)
y = abs(f[1:len(f)/2])
dt = x[-1] - x[0]
x = np.linspace(0, 0.5*len(x)/dt, len(y))
return x, y
def dataType(obj):
if hasattr(obj, '__len__') and len(obj) == 0:

View File

@ -256,6 +256,11 @@ class PlotItem(GraphicsWidget):
c.logYCheck.toggled.connect(self.updateLogMode)
c.downsampleSpin.valueChanged.connect(self.updateDownsampling)
c.downsampleCheck.toggled.connect(self.updateDownsampling)
c.autoDownsampleCheck.toggled.connect(self.updateDownsampling)
c.subsampleRadio.toggled.connect(self.updateDownsampling)
c.meanRadio.toggled.connect(self.updateDownsampling)
c.clipToViewCheck.toggled.connect(self.updateDownsampling)
self.ctrl.avgParamList.itemClicked.connect(self.avgParamListClicked)
self.ctrl.averageGroup.toggled.connect(self.avgToggled)
@ -295,19 +300,21 @@ class PlotItem(GraphicsWidget):
def setLogMode(self, x, y):
def setLogMode(self, x=None, y=None):
"""
Set log scaling for x and y axes.
Set log scaling for x and/or y axes.
This informs PlotDataItems to transform logarithmically and switches
the axes to use log ticking.
Note that *no other items* in the scene will be affected by
this; there is no generic way to redisplay a GraphicsItem
this; there is (currently) no generic way to redisplay a GraphicsItem
with log coordinates.
"""
self.ctrl.logXCheck.setChecked(x)
self.ctrl.logYCheck.setChecked(y)
if x is not None:
self.ctrl.logXCheck.setChecked(x)
if y is not None:
self.ctrl.logYCheck.setChecked(y)
def showGrid(self, x=None, y=None, alpha=None):
"""
@ -526,7 +533,8 @@ class PlotItem(GraphicsWidget):
(alpha, auto) = self.alphaState()
item.setAlpha(alpha, auto)
item.setFftMode(self.ctrl.fftCheck.isChecked())
item.setDownsampling(self.downsampleMode())
item.setDownsampling(*self.downsampleMode())
item.setClipToView(self.clipToViewMode())
item.setPointMode(self.pointMode())
## Hide older plots if needed
@ -568,8 +576,8 @@ class PlotItem(GraphicsWidget):
:func:`InfiniteLine.__init__() <pyqtgraph.InfiniteLine.__init__>`.
Returns the item created.
"""
angle = 0 if x is None else 90
pos = x if x is not None else y
pos = kwds.get('pos', x if x is not None else y)
angle = kwds.get('angle', 0 if x is None else 90)
line = InfiniteLine(pos, angle, **kwds)
self.addItem(line)
if z is not None:
@ -941,23 +949,81 @@ class PlotItem(GraphicsWidget):
self.enableAutoRange()
self.recomputeAverages()
def setDownsampling(self, ds=None, auto=None, mode=None):
"""Change the default downsampling mode for all PlotDataItems managed by this plot.
=========== =================================================================
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
mode 'subsample': Downsample by taking the first of N samples.
This method is fastest and least accurate.
'mean': Downsample by taking the mean of N samples.
'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:
self.ctrl.downsampleCheck.setChecked(False)
elif ds is True:
self.ctrl.downsampleCheck.setChecked(True)
else:
self.ctrl.downsampleCheck.setChecked(True)
self.ctrl.downsampleSpin.setValue(ds)
if auto is not None:
if auto and ds is not False:
self.ctrl.downsampleCheck.setChecked(True)
self.ctrl.autoDownsampleCheck.setChecked(auto)
if mode is not None:
if mode == 'subsample':
self.ctrl.subsampleRadio.setChecked(True)
elif mode == 'mean':
self.ctrl.meanRadio.setChecked(True)
elif mode == 'peak':
self.ctrl.peakRadio.setChecked(True)
else:
raise ValueError("mode argument must be 'subsample', 'mean', or 'peak'.")
def updateDownsampling(self):
ds = self.downsampleMode()
ds, auto, method = self.downsampleMode()
clip = self.ctrl.clipToViewCheck.isChecked()
for c in self.curves:
c.setDownsampling(ds)
c.setDownsampling(ds, auto, method)
c.setClipToView(clip)
self.recomputeAverages()
def downsampleMode(self):
if self.ctrl.decimateGroup.isChecked():
if self.ctrl.manualDecimateRadio.isChecked():
ds = self.ctrl.downsampleSpin.value()
else:
ds = True
if self.ctrl.downsampleCheck.isChecked():
ds = self.ctrl.downsampleSpin.value()
else:
ds = False
return ds
ds = 1
auto = self.ctrl.downsampleCheck.isChecked() and self.ctrl.autoDownsampleCheck.isChecked()
if self.ctrl.subsampleRadio.isChecked():
method = 'subsample'
elif self.ctrl.meanRadio.isChecked():
method = 'mean'
elif self.ctrl.peakRadio.isChecked():
method = 'peak'
return ds, auto, method
def setClipToView(self, clip):
"""Set the default clip-to-view mode for all PlotDataItems managed by this plot.
If *clip* is True, then PlotDataItems will attempt to draw only points within the visible
range of the ViewBox."""
self.ctrl.clipToViewCheck.setChecked(clip)
def clipToViewMode(self):
return self.ctrl.clipToViewCheck.isChecked()
def updateDecimation(self):
if self.ctrl.maxTracesCheck.isChecked():
@ -1079,6 +1145,7 @@ class PlotItem(GraphicsWidget):
============= =================================================================
"""
self.getAxis(axis).setLabel(text=text, units=units, **args)
self.showAxis(axis)
def setLabels(self, **kwds):
"""

View File

@ -6,8 +6,8 @@
<rect>
<x>0</x>
<y>0</y>
<width>258</width>
<height>605</height>
<width>481</width>
<height>840</height>
</rect>
</property>
<property name="windowTitle">
@ -16,8 +16,8 @@
<widget class="QGroupBox" name="averageGroup">
<property name="geometry">
<rect>
<x>10</x>
<y>200</y>
<x>0</x>
<y>640</y>
<width>242</width>
<height>182</height>
</rect>
@ -46,21 +46,15 @@
</item>
</layout>
</widget>
<widget class="QGroupBox" name="decimateGroup">
<widget class="QFrame" name="decimateGroup">
<property name="geometry">
<rect>
<x>0</x>
<y>70</y>
<width>242</width>
<height>160</height>
<x>10</x>
<y>140</y>
<width>191</width>
<height>171</height>
</rect>
</property>
<property name="title">
<string>Downsample</string>
</property>
<property name="checkable">
<bool>true</bool>
</property>
<layout class="QGridLayout" name="gridLayout_4">
<property name="margin">
<number>0</number>
@ -68,40 +62,17 @@
<property name="spacing">
<number>0</number>
</property>
<item row="0" column="0">
<widget class="QRadioButton" name="manualDecimateRadio">
<item row="7" column="0" colspan="3">
<widget class="QCheckBox" name="clipToViewCheck">
<property name="toolTip">
<string>Plot only the portion of each curve that is visible. This assumes X values are uniformly spaced.</string>
</property>
<property name="text">
<string>Manual</string>
</property>
<property name="checked">
<bool>true</bool>
<string>Clip to View</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QSpinBox" name="downsampleSpin">
<property name="minimum">
<number>1</number>
</property>
<property name="maximum">
<number>100000</number>
</property>
<property name="value">
<number>1</number>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QRadioButton" name="autoDecimateRadio">
<property name="text">
<string>Auto</string>
</property>
<property name="checked">
<bool>false</bool>
</property>
</widget>
</item>
<item row="2" column="0">
<item row="8" column="0" colspan="2">
<widget class="QCheckBox" name="maxTracesCheck">
<property name="toolTip">
<string>If multiple curves are displayed in this plot, check this box to limit the number of traces that are displayed.</string>
@ -111,14 +82,34 @@
</property>
</widget>
</item>
<item row="2" column="1">
<item row="0" column="0" colspan="3">
<widget class="QCheckBox" name="downsampleCheck">
<property name="text">
<string>Downsample</string>
</property>
</widget>
</item>
<item row="6" column="1" colspan="2">
<widget class="QRadioButton" name="peakRadio">
<property name="toolTip">
<string>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.</string>
</property>
<property name="text">
<string>Peak</string>
</property>
<property name="checked">
<bool>true</bool>
</property>
</widget>
</item>
<item row="8" column="2">
<widget class="QSpinBox" name="maxTracesSpin">
<property name="toolTip">
<string>If multiple curves are displayed in this plot, check &quot;Max Traces&quot; and set this value to limit the number of traces that are displayed.</string>
</property>
</widget>
</item>
<item row="3" column="0" colspan="2">
<item row="9" column="0" colspan="3">
<widget class="QCheckBox" name="forgetTracesCheck">
<property name="toolTip">
<string>If MaxTraces is checked, remove curves from memory after they are hidden (saves memory, but traces can not be un-hidden).</string>
@ -128,6 +119,74 @@
</property>
</widget>
</item>
<item row="3" column="1" colspan="2">
<widget class="QRadioButton" name="meanRadio">
<property name="toolTip">
<string>Downsample by taking the mean of N samples.</string>
</property>
<property name="text">
<string>Mean</string>
</property>
</widget>
</item>
<item row="2" column="1" colspan="2">
<widget class="QRadioButton" name="subsampleRadio">
<property name="toolTip">
<string>Downsample by taking the first of N samples. This method is fastest and least accurate.</string>
</property>
<property name="text">
<string>Subsample</string>
</property>
</widget>
</item>
<item row="1" column="2">
<widget class="QCheckBox" name="autoDownsampleCheck">
<property name="toolTip">
<string>Automatically downsample data based on the visible range. This assumes X values are uniformly spaced.</string>
</property>
<property name="text">
<string>Auto</string>
</property>
<property name="checked">
<bool>true</bool>
</property>
</widget>
</item>
<item row="2" column="0">
<spacer name="horizontalSpacer">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeType">
<enum>QSizePolicy::Maximum</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>30</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item row="1" column="1">
<widget class="QSpinBox" name="downsampleSpin">
<property name="toolTip">
<string>Downsample data before plotting. (plot every Nth sample)</string>
</property>
<property name="suffix">
<string>x</string>
</property>
<property name="minimum">
<number>1</number>
</property>
<property name="maximum">
<number>100000</number>
</property>
<property name="value">
<number>1</number>
</property>
</widget>
</item>
</layout>
</widget>
<widget class="QFrame" name="transformGroup">

View File

@ -1,9 +1,9 @@
# -*- coding: utf-8 -*-
# Form implementation generated from reading ui file './graphicsItems/PlotItem/plotConfigTemplate.ui'
# Form implementation generated from reading ui file './pyqtgraph/graphicsItems/PlotItem/plotConfigTemplate.ui'
#
# Created: Sun Sep 9 14:41:32 2012
# by: PyQt4 UI code generator 4.9.1
# Created: Mon Jul 1 23:21:08 2013
# by: PyQt4 UI code generator 4.9.3
#
# WARNING! All changes made in this file will be lost!
@ -17,9 +17,9 @@ except AttributeError:
class Ui_Form(object):
def setupUi(self, Form):
Form.setObjectName(_fromUtf8("Form"))
Form.resize(258, 605)
Form.resize(481, 840)
self.averageGroup = QtGui.QGroupBox(Form)
self.averageGroup.setGeometry(QtCore.QRect(10, 200, 242, 182))
self.averageGroup.setGeometry(QtCore.QRect(0, 640, 242, 182))
self.averageGroup.setCheckable(True)
self.averageGroup.setChecked(False)
self.averageGroup.setObjectName(_fromUtf8("averageGroup"))
@ -30,37 +30,50 @@ class Ui_Form(object):
self.avgParamList = QtGui.QListWidget(self.averageGroup)
self.avgParamList.setObjectName(_fromUtf8("avgParamList"))
self.gridLayout_5.addWidget(self.avgParamList, 0, 0, 1, 1)
self.decimateGroup = QtGui.QGroupBox(Form)
self.decimateGroup.setGeometry(QtCore.QRect(0, 70, 242, 160))
self.decimateGroup.setCheckable(True)
self.decimateGroup = QtGui.QFrame(Form)
self.decimateGroup.setGeometry(QtCore.QRect(10, 140, 191, 171))
self.decimateGroup.setObjectName(_fromUtf8("decimateGroup"))
self.gridLayout_4 = QtGui.QGridLayout(self.decimateGroup)
self.gridLayout_4.setMargin(0)
self.gridLayout_4.setSpacing(0)
self.gridLayout_4.setObjectName(_fromUtf8("gridLayout_4"))
self.manualDecimateRadio = QtGui.QRadioButton(self.decimateGroup)
self.manualDecimateRadio.setChecked(True)
self.manualDecimateRadio.setObjectName(_fromUtf8("manualDecimateRadio"))
self.gridLayout_4.addWidget(self.manualDecimateRadio, 0, 0, 1, 1)
self.clipToViewCheck = QtGui.QCheckBox(self.decimateGroup)
self.clipToViewCheck.setObjectName(_fromUtf8("clipToViewCheck"))
self.gridLayout_4.addWidget(self.clipToViewCheck, 7, 0, 1, 3)
self.maxTracesCheck = QtGui.QCheckBox(self.decimateGroup)
self.maxTracesCheck.setObjectName(_fromUtf8("maxTracesCheck"))
self.gridLayout_4.addWidget(self.maxTracesCheck, 8, 0, 1, 2)
self.downsampleCheck = QtGui.QCheckBox(self.decimateGroup)
self.downsampleCheck.setObjectName(_fromUtf8("downsampleCheck"))
self.gridLayout_4.addWidget(self.downsampleCheck, 0, 0, 1, 3)
self.peakRadio = QtGui.QRadioButton(self.decimateGroup)
self.peakRadio.setChecked(True)
self.peakRadio.setObjectName(_fromUtf8("peakRadio"))
self.gridLayout_4.addWidget(self.peakRadio, 6, 1, 1, 2)
self.maxTracesSpin = QtGui.QSpinBox(self.decimateGroup)
self.maxTracesSpin.setObjectName(_fromUtf8("maxTracesSpin"))
self.gridLayout_4.addWidget(self.maxTracesSpin, 8, 2, 1, 1)
self.forgetTracesCheck = QtGui.QCheckBox(self.decimateGroup)
self.forgetTracesCheck.setObjectName(_fromUtf8("forgetTracesCheck"))
self.gridLayout_4.addWidget(self.forgetTracesCheck, 9, 0, 1, 3)
self.meanRadio = QtGui.QRadioButton(self.decimateGroup)
self.meanRadio.setObjectName(_fromUtf8("meanRadio"))
self.gridLayout_4.addWidget(self.meanRadio, 3, 1, 1, 2)
self.subsampleRadio = QtGui.QRadioButton(self.decimateGroup)
self.subsampleRadio.setObjectName(_fromUtf8("subsampleRadio"))
self.gridLayout_4.addWidget(self.subsampleRadio, 2, 1, 1, 2)
self.autoDownsampleCheck = QtGui.QCheckBox(self.decimateGroup)
self.autoDownsampleCheck.setChecked(True)
self.autoDownsampleCheck.setObjectName(_fromUtf8("autoDownsampleCheck"))
self.gridLayout_4.addWidget(self.autoDownsampleCheck, 1, 2, 1, 1)
spacerItem = QtGui.QSpacerItem(30, 20, QtGui.QSizePolicy.Maximum, QtGui.QSizePolicy.Minimum)
self.gridLayout_4.addItem(spacerItem, 2, 0, 1, 1)
self.downsampleSpin = QtGui.QSpinBox(self.decimateGroup)
self.downsampleSpin.setMinimum(1)
self.downsampleSpin.setMaximum(100000)
self.downsampleSpin.setProperty("value", 1)
self.downsampleSpin.setObjectName(_fromUtf8("downsampleSpin"))
self.gridLayout_4.addWidget(self.downsampleSpin, 0, 1, 1, 1)
self.autoDecimateRadio = QtGui.QRadioButton(self.decimateGroup)
self.autoDecimateRadio.setChecked(False)
self.autoDecimateRadio.setObjectName(_fromUtf8("autoDecimateRadio"))
self.gridLayout_4.addWidget(self.autoDecimateRadio, 1, 0, 1, 1)
self.maxTracesCheck = QtGui.QCheckBox(self.decimateGroup)
self.maxTracesCheck.setObjectName(_fromUtf8("maxTracesCheck"))
self.gridLayout_4.addWidget(self.maxTracesCheck, 2, 0, 1, 1)
self.maxTracesSpin = QtGui.QSpinBox(self.decimateGroup)
self.maxTracesSpin.setObjectName(_fromUtf8("maxTracesSpin"))
self.gridLayout_4.addWidget(self.maxTracesSpin, 2, 1, 1, 1)
self.forgetTracesCheck = QtGui.QCheckBox(self.decimateGroup)
self.forgetTracesCheck.setObjectName(_fromUtf8("forgetTracesCheck"))
self.gridLayout_4.addWidget(self.forgetTracesCheck, 3, 0, 1, 2)
self.gridLayout_4.addWidget(self.downsampleSpin, 1, 1, 1, 1)
self.transformGroup = QtGui.QFrame(Form)
self.transformGroup.setGeometry(QtCore.QRect(0, 0, 154, 79))
self.transformGroup.setObjectName(_fromUtf8("transformGroup"))
@ -129,14 +142,24 @@ class Ui_Form(object):
Form.setWindowTitle(QtGui.QApplication.translate("Form", "Form", None, QtGui.QApplication.UnicodeUTF8))
self.averageGroup.setToolTip(QtGui.QApplication.translate("Form", "Display averages of the curves displayed in this plot. The parameter list allows you to choose parameters to average over (if any are available).", None, QtGui.QApplication.UnicodeUTF8))
self.averageGroup.setTitle(QtGui.QApplication.translate("Form", "Average", None, QtGui.QApplication.UnicodeUTF8))
self.decimateGroup.setTitle(QtGui.QApplication.translate("Form", "Downsample", None, QtGui.QApplication.UnicodeUTF8))
self.manualDecimateRadio.setText(QtGui.QApplication.translate("Form", "Manual", None, QtGui.QApplication.UnicodeUTF8))
self.autoDecimateRadio.setText(QtGui.QApplication.translate("Form", "Auto", None, QtGui.QApplication.UnicodeUTF8))
self.clipToViewCheck.setToolTip(QtGui.QApplication.translate("Form", "Plot only the portion of each curve that is visible. This assumes X values are uniformly spaced.", None, QtGui.QApplication.UnicodeUTF8))
self.clipToViewCheck.setText(QtGui.QApplication.translate("Form", "Clip to View", None, QtGui.QApplication.UnicodeUTF8))
self.maxTracesCheck.setToolTip(QtGui.QApplication.translate("Form", "If multiple curves are displayed in this plot, check this box to limit the number of traces that are displayed.", None, QtGui.QApplication.UnicodeUTF8))
self.maxTracesCheck.setText(QtGui.QApplication.translate("Form", "Max Traces:", None, QtGui.QApplication.UnicodeUTF8))
self.downsampleCheck.setText(QtGui.QApplication.translate("Form", "Downsample", None, QtGui.QApplication.UnicodeUTF8))
self.peakRadio.setToolTip(QtGui.QApplication.translate("Form", "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.", None, QtGui.QApplication.UnicodeUTF8))
self.peakRadio.setText(QtGui.QApplication.translate("Form", "Peak", None, QtGui.QApplication.UnicodeUTF8))
self.maxTracesSpin.setToolTip(QtGui.QApplication.translate("Form", "If multiple curves are displayed in this plot, check \"Max Traces\" and set this value to limit the number of traces that are displayed.", None, QtGui.QApplication.UnicodeUTF8))
self.forgetTracesCheck.setToolTip(QtGui.QApplication.translate("Form", "If MaxTraces is checked, remove curves from memory after they are hidden (saves memory, but traces can not be un-hidden).", None, QtGui.QApplication.UnicodeUTF8))
self.forgetTracesCheck.setText(QtGui.QApplication.translate("Form", "Forget hidden traces", None, QtGui.QApplication.UnicodeUTF8))
self.meanRadio.setToolTip(QtGui.QApplication.translate("Form", "Downsample by taking the mean of N samples.", None, QtGui.QApplication.UnicodeUTF8))
self.meanRadio.setText(QtGui.QApplication.translate("Form", "Mean", None, QtGui.QApplication.UnicodeUTF8))
self.subsampleRadio.setToolTip(QtGui.QApplication.translate("Form", "Downsample by taking the first of N samples. This method is fastest and least accurate.", None, QtGui.QApplication.UnicodeUTF8))
self.subsampleRadio.setText(QtGui.QApplication.translate("Form", "Subsample", None, QtGui.QApplication.UnicodeUTF8))
self.autoDownsampleCheck.setToolTip(QtGui.QApplication.translate("Form", "Automatically downsample data based on the visible range. This assumes X values are uniformly spaced.", None, QtGui.QApplication.UnicodeUTF8))
self.autoDownsampleCheck.setText(QtGui.QApplication.translate("Form", "Auto", None, QtGui.QApplication.UnicodeUTF8))
self.downsampleSpin.setToolTip(QtGui.QApplication.translate("Form", "Downsample data before plotting. (plot every Nth sample)", None, QtGui.QApplication.UnicodeUTF8))
self.downsampleSpin.setSuffix(QtGui.QApplication.translate("Form", "x", None, QtGui.QApplication.UnicodeUTF8))
self.fftCheck.setText(QtGui.QApplication.translate("Form", "Power Spectrum (FFT)", None, QtGui.QApplication.UnicodeUTF8))
self.logXCheck.setText(QtGui.QApplication.translate("Form", "Log X", None, QtGui.QApplication.UnicodeUTF8))
self.logYCheck.setText(QtGui.QApplication.translate("Form", "Log Y", None, QtGui.QApplication.UnicodeUTF8))

View File

@ -1,9 +1,9 @@
# -*- coding: utf-8 -*-
# Form implementation generated from reading ui file './graphicsItems/PlotItem/plotConfigTemplate.ui'
# Form implementation generated from reading ui file './pyqtgraph/graphicsItems/PlotItem/plotConfigTemplate.ui'
#
# Created: Sun Sep 9 14:41:32 2012
# by: pyside-uic 0.2.13 running on PySide 1.1.0
# Created: Mon Jul 1 23:21:08 2013
# by: pyside-uic 0.2.13 running on PySide 1.1.2
#
# WARNING! All changes made in this file will be lost!
@ -12,9 +12,9 @@ from PySide import QtCore, QtGui
class Ui_Form(object):
def setupUi(self, Form):
Form.setObjectName("Form")
Form.resize(258, 605)
Form.resize(481, 840)
self.averageGroup = QtGui.QGroupBox(Form)
self.averageGroup.setGeometry(QtCore.QRect(10, 200, 242, 182))
self.averageGroup.setGeometry(QtCore.QRect(0, 640, 242, 182))
self.averageGroup.setCheckable(True)
self.averageGroup.setChecked(False)
self.averageGroup.setObjectName("averageGroup")
@ -25,37 +25,50 @@ class Ui_Form(object):
self.avgParamList = QtGui.QListWidget(self.averageGroup)
self.avgParamList.setObjectName("avgParamList")
self.gridLayout_5.addWidget(self.avgParamList, 0, 0, 1, 1)
self.decimateGroup = QtGui.QGroupBox(Form)
self.decimateGroup.setGeometry(QtCore.QRect(0, 70, 242, 160))
self.decimateGroup.setCheckable(True)
self.decimateGroup = QtGui.QFrame(Form)
self.decimateGroup.setGeometry(QtCore.QRect(10, 140, 191, 171))
self.decimateGroup.setObjectName("decimateGroup")
self.gridLayout_4 = QtGui.QGridLayout(self.decimateGroup)
self.gridLayout_4.setContentsMargins(0, 0, 0, 0)
self.gridLayout_4.setSpacing(0)
self.gridLayout_4.setObjectName("gridLayout_4")
self.manualDecimateRadio = QtGui.QRadioButton(self.decimateGroup)
self.manualDecimateRadio.setChecked(True)
self.manualDecimateRadio.setObjectName("manualDecimateRadio")
self.gridLayout_4.addWidget(self.manualDecimateRadio, 0, 0, 1, 1)
self.clipToViewCheck = QtGui.QCheckBox(self.decimateGroup)
self.clipToViewCheck.setObjectName("clipToViewCheck")
self.gridLayout_4.addWidget(self.clipToViewCheck, 7, 0, 1, 3)
self.maxTracesCheck = QtGui.QCheckBox(self.decimateGroup)
self.maxTracesCheck.setObjectName("maxTracesCheck")
self.gridLayout_4.addWidget(self.maxTracesCheck, 8, 0, 1, 2)
self.downsampleCheck = QtGui.QCheckBox(self.decimateGroup)
self.downsampleCheck.setObjectName("downsampleCheck")
self.gridLayout_4.addWidget(self.downsampleCheck, 0, 0, 1, 3)
self.peakRadio = QtGui.QRadioButton(self.decimateGroup)
self.peakRadio.setChecked(True)
self.peakRadio.setObjectName("peakRadio")
self.gridLayout_4.addWidget(self.peakRadio, 6, 1, 1, 2)
self.maxTracesSpin = QtGui.QSpinBox(self.decimateGroup)
self.maxTracesSpin.setObjectName("maxTracesSpin")
self.gridLayout_4.addWidget(self.maxTracesSpin, 8, 2, 1, 1)
self.forgetTracesCheck = QtGui.QCheckBox(self.decimateGroup)
self.forgetTracesCheck.setObjectName("forgetTracesCheck")
self.gridLayout_4.addWidget(self.forgetTracesCheck, 9, 0, 1, 3)
self.meanRadio = QtGui.QRadioButton(self.decimateGroup)
self.meanRadio.setObjectName("meanRadio")
self.gridLayout_4.addWidget(self.meanRadio, 3, 1, 1, 2)
self.subsampleRadio = QtGui.QRadioButton(self.decimateGroup)
self.subsampleRadio.setObjectName("subsampleRadio")
self.gridLayout_4.addWidget(self.subsampleRadio, 2, 1, 1, 2)
self.autoDownsampleCheck = QtGui.QCheckBox(self.decimateGroup)
self.autoDownsampleCheck.setChecked(True)
self.autoDownsampleCheck.setObjectName("autoDownsampleCheck")
self.gridLayout_4.addWidget(self.autoDownsampleCheck, 1, 2, 1, 1)
spacerItem = QtGui.QSpacerItem(30, 20, QtGui.QSizePolicy.Maximum, QtGui.QSizePolicy.Minimum)
self.gridLayout_4.addItem(spacerItem, 2, 0, 1, 1)
self.downsampleSpin = QtGui.QSpinBox(self.decimateGroup)
self.downsampleSpin.setMinimum(1)
self.downsampleSpin.setMaximum(100000)
self.downsampleSpin.setProperty("value", 1)
self.downsampleSpin.setObjectName("downsampleSpin")
self.gridLayout_4.addWidget(self.downsampleSpin, 0, 1, 1, 1)
self.autoDecimateRadio = QtGui.QRadioButton(self.decimateGroup)
self.autoDecimateRadio.setChecked(False)
self.autoDecimateRadio.setObjectName("autoDecimateRadio")
self.gridLayout_4.addWidget(self.autoDecimateRadio, 1, 0, 1, 1)
self.maxTracesCheck = QtGui.QCheckBox(self.decimateGroup)
self.maxTracesCheck.setObjectName("maxTracesCheck")
self.gridLayout_4.addWidget(self.maxTracesCheck, 2, 0, 1, 1)
self.maxTracesSpin = QtGui.QSpinBox(self.decimateGroup)
self.maxTracesSpin.setObjectName("maxTracesSpin")
self.gridLayout_4.addWidget(self.maxTracesSpin, 2, 1, 1, 1)
self.forgetTracesCheck = QtGui.QCheckBox(self.decimateGroup)
self.forgetTracesCheck.setObjectName("forgetTracesCheck")
self.gridLayout_4.addWidget(self.forgetTracesCheck, 3, 0, 1, 2)
self.gridLayout_4.addWidget(self.downsampleSpin, 1, 1, 1, 1)
self.transformGroup = QtGui.QFrame(Form)
self.transformGroup.setGeometry(QtCore.QRect(0, 0, 154, 79))
self.transformGroup.setObjectName("transformGroup")
@ -124,14 +137,24 @@ class Ui_Form(object):
Form.setWindowTitle(QtGui.QApplication.translate("Form", "Form", None, QtGui.QApplication.UnicodeUTF8))
self.averageGroup.setToolTip(QtGui.QApplication.translate("Form", "Display averages of the curves displayed in this plot. The parameter list allows you to choose parameters to average over (if any are available).", None, QtGui.QApplication.UnicodeUTF8))
self.averageGroup.setTitle(QtGui.QApplication.translate("Form", "Average", None, QtGui.QApplication.UnicodeUTF8))
self.decimateGroup.setTitle(QtGui.QApplication.translate("Form", "Downsample", None, QtGui.QApplication.UnicodeUTF8))
self.manualDecimateRadio.setText(QtGui.QApplication.translate("Form", "Manual", None, QtGui.QApplication.UnicodeUTF8))
self.autoDecimateRadio.setText(QtGui.QApplication.translate("Form", "Auto", None, QtGui.QApplication.UnicodeUTF8))
self.clipToViewCheck.setToolTip(QtGui.QApplication.translate("Form", "Plot only the portion of each curve that is visible. This assumes X values are uniformly spaced.", None, QtGui.QApplication.UnicodeUTF8))
self.clipToViewCheck.setText(QtGui.QApplication.translate("Form", "Clip to View", None, QtGui.QApplication.UnicodeUTF8))
self.maxTracesCheck.setToolTip(QtGui.QApplication.translate("Form", "If multiple curves are displayed in this plot, check this box to limit the number of traces that are displayed.", None, QtGui.QApplication.UnicodeUTF8))
self.maxTracesCheck.setText(QtGui.QApplication.translate("Form", "Max Traces:", None, QtGui.QApplication.UnicodeUTF8))
self.downsampleCheck.setText(QtGui.QApplication.translate("Form", "Downsample", None, QtGui.QApplication.UnicodeUTF8))
self.peakRadio.setToolTip(QtGui.QApplication.translate("Form", "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.", None, QtGui.QApplication.UnicodeUTF8))
self.peakRadio.setText(QtGui.QApplication.translate("Form", "Peak", None, QtGui.QApplication.UnicodeUTF8))
self.maxTracesSpin.setToolTip(QtGui.QApplication.translate("Form", "If multiple curves are displayed in this plot, check \"Max Traces\" and set this value to limit the number of traces that are displayed.", None, QtGui.QApplication.UnicodeUTF8))
self.forgetTracesCheck.setToolTip(QtGui.QApplication.translate("Form", "If MaxTraces is checked, remove curves from memory after they are hidden (saves memory, but traces can not be un-hidden).", None, QtGui.QApplication.UnicodeUTF8))
self.forgetTracesCheck.setText(QtGui.QApplication.translate("Form", "Forget hidden traces", None, QtGui.QApplication.UnicodeUTF8))
self.meanRadio.setToolTip(QtGui.QApplication.translate("Form", "Downsample by taking the mean of N samples.", None, QtGui.QApplication.UnicodeUTF8))
self.meanRadio.setText(QtGui.QApplication.translate("Form", "Mean", None, QtGui.QApplication.UnicodeUTF8))
self.subsampleRadio.setToolTip(QtGui.QApplication.translate("Form", "Downsample by taking the first of N samples. This method is fastest and least accurate.", None, QtGui.QApplication.UnicodeUTF8))
self.subsampleRadio.setText(QtGui.QApplication.translate("Form", "Subsample", None, QtGui.QApplication.UnicodeUTF8))
self.autoDownsampleCheck.setToolTip(QtGui.QApplication.translate("Form", "Automatically downsample data based on the visible range. This assumes X values are uniformly spaced.", None, QtGui.QApplication.UnicodeUTF8))
self.autoDownsampleCheck.setText(QtGui.QApplication.translate("Form", "Auto", None, QtGui.QApplication.UnicodeUTF8))
self.downsampleSpin.setToolTip(QtGui.QApplication.translate("Form", "Downsample data before plotting. (plot every Nth sample)", None, QtGui.QApplication.UnicodeUTF8))
self.downsampleSpin.setSuffix(QtGui.QApplication.translate("Form", "x", None, QtGui.QApplication.UnicodeUTF8))
self.fftCheck.setText(QtGui.QApplication.translate("Form", "Power Spectrum (FFT)", None, QtGui.QApplication.UnicodeUTF8))
self.logXCheck.setText(QtGui.QApplication.translate("Form", "Log X", None, QtGui.QApplication.UnicodeUTF8))
self.logYCheck.setText(QtGui.QApplication.translate("Form", "Log Y", None, QtGui.QApplication.UnicodeUTF8))

View File

@ -38,6 +38,27 @@ def rectStr(r):
class ROI(GraphicsObject):
"""Generic region-of-interest widget.
Can be used for implementing many types of selection box with rotate/translate/scale handles.
Signals
----------------------- ----------------------------------------------------
sigRegionChangeFinished Emitted when the user stops dragging the ROI (or
one of its handles) or if the ROI is changed
programatically.
sigRegionChangeStarted Emitted when the user starts dragging the ROI (or
one of its handles).
sigRegionChanged Emitted any time the position of the ROI changes,
including while it is being dragged by the user.
sigHoverEvent Emitted when the mouse hovers over the ROI.
sigClicked Emitted when the user clicks on the ROI.
Note that clicking is disabled by default to prevent
stealing clicks from objects behind the ROI. To
enable clicking, call
roi.setAcceptedMouseButtons(QtCore.Qt.LeftButton).
See QtGui.QGraphicsItem documentation for more
details.
sigRemoveRequested Emitted when the user selects 'remove' from the
ROI's context menu (if available).
----------------------- ----------------------------------------------------
"""
sigRegionChangeFinished = QtCore.Signal(object)
@ -802,7 +823,11 @@ class ROI(GraphicsObject):
Also returns the transform which maps the ROI into data coordinates.
If returnSlice is set to False, the function returns a pair of tuples with the values that would have
been used to generate the slice objects. ((ax0Start, ax0Stop), (ax1Start, ax1Stop))"""
been used to generate the slice objects. ((ax0Start, ax0Stop), (ax1Start, ax1Stop))
If the slice can not be computed (usually because the scene/transforms are not properly
constructed yet), then the method returns None.
"""
#print "getArraySlice"
## Determine shape of array along ROI axes
@ -810,8 +835,11 @@ class ROI(GraphicsObject):
#print " dshape", dShape
## Determine transform that maps ROI bounding box to image coordinates
tr = self.sceneTransform() * fn.invertQTransform(img.sceneTransform())
try:
tr = self.sceneTransform() * fn.invertQTransform(img.sceneTransform())
except np.linalg.linalg.LinAlgError:
return None
## Modify transform to scale from image coords to data coords
#m = QtGui.QTransform()
tr.scale(float(dShape[0]) / img.width(), float(dShape[1]) / img.height())
@ -1292,7 +1320,6 @@ class Handle(UIGraphicsItem):
## determine rotation of transform
#m = self.sceneTransform() ## Qt bug: do not access sceneTransform() until we know this object has a scene.
#mi = m.inverted()[0]
dt = self.deviceTransform()
if dt is None:
@ -1311,10 +1338,10 @@ class Handle(UIGraphicsItem):
return dti.map(tr.map(self.path))
def viewRangeChanged(self):
GraphicsObject.viewRangeChanged(self)
def viewTransformChanged(self):
GraphicsObject.viewTransformChanged(self)
self._shape = None ## invalidate shape, recompute later if requested.
#self.updateShape()
self.update()
#def itemChange(self, change, value):
#if change == self.ItemScenePositionHasChanged:
@ -1599,7 +1626,7 @@ class PolyLineROI(ROI):
if pos is None:
pos = [0,0]
#pen=args.get('pen', fn.mkPen((100,100,255)))
ROI.__init__(self, pos, size=[1,1], **args)
self.closed = closed
self.segments = []
@ -1610,33 +1637,6 @@ class PolyLineROI(ROI):
start = -1 if self.closed else 0
for i in range(start, len(self.handles)-1):
self.addSegment(self.handles[i]['item'], self.handles[i+1]['item'])
#for i in range(len(positions)-1):
#h2 = self.addFreeHandle(positions[i+1])
#segment = LineSegmentROI(handles=(h, h2), pen=pen, parent=self, movable=False)
#self.segments.append(segment)
#h = h2
#for i, s in enumerate(self.segments):
#h = s.handles[0]
#self.addFreeHandle(h['pos'], item=h['item'])
#s.setZValue(self.zValue() +1)
#h = self.segments[-1].handles[1]
#self.addFreeHandle(h['pos'], item=h['item'])
#if closed:
#h1 = self.handles[-1]['item']
#h2 = self.handles[0]['item']
#self.segments.append(LineSegmentROI([positions[-1], positions[0]], pos=pos, handles=(h1, h2), pen=pen, parent=self, movable=False))
#h2.setParentItem(self.segments[-1])
#for s in self.segments:
#self.setSegmentSettings(s)
#def movePoint(self, *args, **kargs):
#pass
def addSegment(self, h1, h2, index=None):
seg = LineSegmentROI(handles=(h1, h2), pen=self.pen, parent=self, movable=False)
@ -1653,9 +1653,6 @@ class PolyLineROI(ROI):
def setMouseHover(self, hover):
## Inform all the ROI's segments that the mouse is(not) hovering over it
#if self.mouseHovering == hover:
#return
#self.mouseHovering = hover
ROI.setMouseHover(self, hover)
for s in self.segments:
s.setMouseHover(hover)
@ -1680,15 +1677,6 @@ class PolyLineROI(ROI):
self.addSegment(h3, h2, index=i+1)
segment.replaceHandle(h2, h3)
#def report(self):
#for s in self.segments:
#print s
#for h in s.handles:
#print " ", h
#for h in self.handles:
#print h
def removeHandle(self, handle, updateSegments=True):
ROI.removeHandle(self, handle)
handle.sigRemoveRequested.disconnect(self.removeHandle)
@ -1737,11 +1725,34 @@ class PolyLineROI(ROI):
def shape(self):
p = QtGui.QPainterPath()
if len(self.handles) == 0:
return p
p.moveTo(self.handles[0]['item'].pos())
for i in range(len(self.handles)):
p.lineTo(self.handles[i]['item'].pos())
p.lineTo(self.handles[0]['item'].pos())
return p
return p
def getArrayRegion(self, data, img, axes=(0,1), returnMappedCoords=False, **kwds):
sl = self.getArraySlice(data, img, axes=(0,1))
if sl is None:
return None
sliced = data[sl[0]]
im = QtGui.QImage(sliced.shape[axes[0]], sliced.shape[axes[1]], QtGui.QImage.Format_ARGB32)
im.fill(0x0)
p = QtGui.QPainter(im)
p.setPen(fn.mkPen(None))
p.setBrush(fn.mkBrush('w'))
p.setTransform(self.itemTransform(img)[0])
bounds = self.mapRectToItem(img, self.boundingRect())
p.translate(-bounds.left(), -bounds.top())
p.drawPath(self.shape())
p.end()
mask = fn.imageToArray(im)[:,:,0].astype(float) / 255.
shape = [1] * data.ndim
shape[axes[0]] = sliced.shape[axes[0]]
shape[axes[1]] = sliced.shape[axes[1]]
return sliced * mask.reshape(shape)
class LineSegmentROI(ROI):
@ -1845,8 +1856,8 @@ class SpiralROI(ROI):
#for h in self.handles:
#h['pos'] = h['item'].pos()/self.state['size'][0]
def stateChanged(self):
ROI.stateChanged(self)
def stateChanged(self, finish=True):
ROI.stateChanged(self, finish=finish)
if len(self.handles) > 1:
self.path = QtGui.QPainterPath()
h0 = Point(self.handles[0]['item'].pos()).length()

View File

@ -1,50 +1,104 @@
from pyqtgraph.Qt import QtGui, QtCore
from .UIGraphicsItem import *
from .GraphicsObject import *
from .GraphicsWidgetAnchor import *
from .TextItem import TextItem
import numpy as np
import pyqtgraph.functions as fn
import pyqtgraph as pg
__all__ = ['ScaleBar']
class ScaleBar(UIGraphicsItem):
class ScaleBar(GraphicsObject, GraphicsWidgetAnchor):
"""
Displays a rectangular bar with 10 divisions to indicate the relative scale of objects on the view.
Displays a rectangular bar to indicate the relative scale of objects on the view.
"""
def __init__(self, size, width=5, color=(100, 100, 255)):
UIGraphicsItem.__init__(self)
def __init__(self, size, width=5, brush=None, pen=None, suffix='m'):
GraphicsObject.__init__(self)
GraphicsWidgetAnchor.__init__(self)
self.setFlag(self.ItemHasNoContents)
self.setAcceptedMouseButtons(QtCore.Qt.NoButton)
self.brush = fn.mkBrush(color)
self.pen = fn.mkPen((0,0,0))
if brush is None:
brush = pg.getConfigOption('foreground')
self.brush = fn.mkBrush(brush)
self.pen = fn.mkPen(pen)
self._width = width
self.size = size
def paint(self, p, opt, widget):
UIGraphicsItem.paint(self, p, opt, widget)
self.bar = QtGui.QGraphicsRectItem()
self.bar.setPen(self.pen)
self.bar.setBrush(self.brush)
self.bar.setParentItem(self)
rect = self.boundingRect()
unit = self.pixelSize()
y = rect.top() + (rect.bottom()-rect.top()) * 0.02
y1 = y + unit[1]*self._width
x = rect.right() + (rect.left()-rect.right()) * 0.02
x1 = x - self.size
self.text = TextItem(text=fn.siFormat(size, suffix=suffix), anchor=(0.5,1))
self.text.setParentItem(self)
def parentChanged(self):
view = self.parentItem()
if view is None:
return
view.sigRangeChanged.connect(self.updateBar)
self.updateBar()
p.setPen(self.pen)
p.setBrush(self.brush)
rect = QtCore.QRectF(
QtCore.QPointF(x1, y1),
QtCore.QPointF(x, y)
)
p.translate(x1, y1)
p.scale(rect.width(), rect.height())
p.drawRect(0, 0, 1, 1)
alpha = np.clip(((self.size/unit[0]) - 40.) * 255. / 80., 0, 255)
p.setPen(QtGui.QPen(QtGui.QColor(0, 0, 0, alpha)))
for i in range(1, 10):
#x2 = x + (x1-x) * 0.1 * i
x2 = 0.1 * i
p.drawLine(QtCore.QPointF(x2, 0), QtCore.QPointF(x2, 1))
def updateBar(self):
view = self.parentItem()
if view is None:
return
p1 = view.mapFromViewToItem(self, QtCore.QPointF(0,0))
p2 = view.mapFromViewToItem(self, QtCore.QPointF(self.size,0))
w = (p2-p1).x()
self.bar.setRect(QtCore.QRectF(-w, 0, w, self._width))
self.text.setPos(-w/2., 0)
def boundingRect(self):
return QtCore.QRectF()
#class ScaleBar(UIGraphicsItem):
#"""
#Displays a rectangular bar with 10 divisions to indicate the relative scale of objects on the view.
#"""
#def __init__(self, size, width=5, color=(100, 100, 255)):
#UIGraphicsItem.__init__(self)
#self.setAcceptedMouseButtons(QtCore.Qt.NoButton)
#self.brush = fn.mkBrush(color)
#self.pen = fn.mkPen((0,0,0))
#self._width = width
#self.size = size
#def paint(self, p, opt, widget):
#UIGraphicsItem.paint(self, p, opt, widget)
#rect = self.boundingRect()
#unit = self.pixelSize()
#y = rect.top() + (rect.bottom()-rect.top()) * 0.02
#y1 = y + unit[1]*self._width
#x = rect.right() + (rect.left()-rect.right()) * 0.02
#x1 = x - self.size
#p.setPen(self.pen)
#p.setBrush(self.brush)
#rect = QtCore.QRectF(
#QtCore.QPointF(x1, y1),
#QtCore.QPointF(x, y)
#)
#p.translate(x1, y1)
#p.scale(rect.width(), rect.height())
#p.drawRect(0, 0, 1, 1)
#alpha = np.clip(((self.size/unit[0]) - 40.) * 255. / 80., 0, 255)
#p.setPen(QtGui.QPen(QtGui.QColor(0, 0, 0, alpha)))
#for i in range(1, 10):
##x2 = x + (x1-x) * 0.1 * i
#x2 = 0.1 * i
#p.drawLine(QtCore.QPointF(x2, 0), QtCore.QPointF(x2, 1))
def setSize(self, s):
self.size = s
#def setSize(self, s):
#self.size = s

View File

@ -4,7 +4,6 @@ import pyqtgraph.functions as fn
from .GraphicsItem import GraphicsItem
from .GraphicsObject import GraphicsObject
import numpy as np
import scipy.stats
import weakref
import pyqtgraph.debug as debug
from pyqtgraph.pgcollections import OrderedDict
@ -15,7 +14,7 @@ __all__ = ['ScatterPlotItem', 'SpotItem']
## Build all symbol paths
Symbols = OrderedDict([(name, QtGui.QPainterPath()) for name in ['o', 's', 't', 'd', '+']])
Symbols = OrderedDict([(name, QtGui.QPainterPath()) for name in ['o', 's', 't', 'd', '+', 'x']])
Symbols['o'].addEllipse(QtCore.QRectF(-0.5, -0.5, 1, 1))
Symbols['s'].addRect(QtCore.QRectF(-0.5, -0.5, 1, 1))
coords = {
@ -32,9 +31,14 @@ for k, c in coords.items():
for x,y in c[1:]:
Symbols[k].lineTo(x, y)
Symbols[k].closeSubpath()
tr = QtGui.QTransform()
tr.rotate(45)
Symbols['x'] = tr.map(Symbols['+'])
def drawSymbol(painter, symbol, size, pen, brush):
if symbol is None:
return
painter.scale(size, size)
painter.setPen(pen)
painter.setBrush(brush)
@ -53,25 +57,17 @@ def renderSymbol(symbol, size, pen, brush, device=None):
the symbol will be rendered into the device specified (See QPainter documentation
for more information).
"""
## see if this pixmap is already cached
#global SymbolPixmapCache
#key = (symbol, size, fn.colorTuple(pen.color()), pen.width(), pen.style(), fn.colorTuple(brush.color()))
#if key in SymbolPixmapCache:
#return SymbolPixmapCache[key]
## Render a spot with the given parameters to a pixmap
penPxWidth = max(np.ceil(pen.widthF()), 1)
image = QtGui.QImage(int(size+penPxWidth), int(size+penPxWidth), QtGui.QImage.Format_ARGB32)
image.fill(0)
p = QtGui.QPainter(image)
if device is None:
device = QtGui.QImage(int(size+penPxWidth), int(size+penPxWidth), QtGui.QImage.Format_ARGB32)
device.fill(0)
p = QtGui.QPainter(device)
p.setRenderHint(p.Antialiasing)
p.translate(image.width()*0.5, image.height()*0.5)
p.translate(device.width()*0.5, device.height()*0.5)
drawSymbol(p, symbol, size, pen, brush)
p.end()
return image
#pixmap = QtGui.QPixmap(image)
#SymbolPixmapCache[key] = pixmap
#return pixmap
return device
def makeSymbolPixmap(size, pen, brush, symbol):
## deprecated
@ -520,7 +516,7 @@ class ScatterPlotItem(GraphicsObject):
## Bug: If data is a numpy record array, then items from that array must be copied to dataSet one at a time.
## (otherwise they are converted to tuples and thus lose their field names.
if isinstance(data, np.ndarray) and len(data.dtype.fields) > 1:
if isinstance(data, np.ndarray) and (data.dtype.fields is not None)and len(data.dtype.fields) > 1:
for i, rec in enumerate(data):
dataSet['data'][i] = rec
else:
@ -629,13 +625,15 @@ class ScatterPlotItem(GraphicsObject):
d2 = d2[mask]
if frac >= 1.0:
self.bounds[ax] = (d.min() - self._maxSpotWidth*0.7072, d.max() + self._maxSpotWidth*0.7072)
self.bounds[ax] = (np.nanmin(d) - self._maxSpotWidth*0.7072, np.nanmax(d) + self._maxSpotWidth*0.7072)
return self.bounds[ax]
elif frac <= 0.0:
raise Exception("Value for parameter 'frac' must be > 0. (got %s)" % str(frac))
else:
return (scipy.stats.scoreatpercentile(d, 50 - (frac * 50)), scipy.stats.scoreatpercentile(d, 50 + (frac * 50)))
mask = np.isfinite(d)
d = d[mask]
return np.percentile(d, [50 * (1 - frac), 50 * (1 + frac)])
def pixelPadding(self):
return self._maxSpotPxWidth*0.7072
@ -677,7 +675,7 @@ class ScatterPlotItem(GraphicsObject):
pts[1] = self.data['y']
pts = fn.transformCoordinates(tr, pts)
self.fragments = []
pts = np.clip(pts, -2**31, 2**31) ## prevent Qt segmentation fault.
pts = np.clip(pts, -2**30, 2**30) ## prevent Qt segmentation fault.
## Still won't be able to render correctly, though.
for i in xrange(len(self.data)):
rec = self.data[i]
@ -689,7 +687,8 @@ class ScatterPlotItem(GraphicsObject):
def setExportMode(self, *args, **kwds):
GraphicsObject.setExportMode(self, *args, **kwds)
self.invalidate()
@pg.debug.warnOnException ## raising an exception here causes crash
def paint(self, p, *args):
#p.setPen(fn.mkPen('r'))
@ -740,6 +739,7 @@ class ScatterPlotItem(GraphicsObject):
drawSymbol(p2, *self.getSpotOpts(rec, scale))
p2.end()
p.setRenderHint(p.Antialiasing, aa)
self.picture.play(p)
def points(self):

View File

@ -17,6 +17,10 @@ __all__ = ['ViewBox']
class ChildGroup(ItemGroup):
sigItemsChanged = QtCore.Signal()
def __init__(self, parent):
ItemGroup.__init__(self, parent)
# 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)
@ -50,6 +54,7 @@ class ViewBox(GraphicsWidget):
#sigActionPositionChanged = QtCore.Signal(object)
sigStateChanged = QtCore.Signal(object)
sigTransformChanged = QtCore.Signal(object)
sigResized = QtCore.Signal(object)
## mouse modes
PanMode = 3
@ -86,6 +91,10 @@ class ViewBox(GraphicsWidget):
self.addedItems = []
#self.gView = view
#self.showGrid = showGrid
self._matrixNeedsUpdate = True ## indicates that range has changed, but matrix update was deferred
self._autoRangeNeedsUpdate = True ## indicates auto-range needs to be recomputed.
self._lastScene = None ## stores reference to the last known scene this view was a part of.
self.state = {
@ -137,9 +146,16 @@ class ViewBox(GraphicsWidget):
self.rbScaleBox = QtGui.QGraphicsRectItem(0, 0, 1, 1)
self.rbScaleBox.setPen(fn.mkPen((255,255,100), width=1))
self.rbScaleBox.setBrush(fn.mkBrush(255,255,0,100))
self.rbScaleBox.setZValue(1e9)
self.rbScaleBox.hide()
self.addItem(self.rbScaleBox, ignoreBounds=True)
## show target rect for debugging
self.target = QtGui.QGraphicsRectItem(0, 0, 1, 1)
self.target.setPen(fn.mkPen('r'))
self.target.setParentItem(self)
self.target.hide()
self.axHistory = [] # maintain a history of zoom locations
self.axHistoryPointer = -1 # pointer into the history. Allows forward/backward movement, not just "undo"
@ -174,7 +190,7 @@ class ViewBox(GraphicsWidget):
def unregister(self):
"""
Remove this ViewBox forom the list of linkable views. (see :func:`register() <pyqtgraph.ViewBox.register>`)
Remove this ViewBox from the list of linkable views. (see :func:`register() <pyqtgraph.ViewBox.register>`)
"""
del ViewBox.AllViews[self]
if self.name is not None:
@ -186,6 +202,48 @@ class ViewBox(GraphicsWidget):
def implements(self, interface):
return interface == 'ViewBox'
# removed due to https://bugreports.qt-project.org/browse/PYSIDE-86
#def itemChange(self, change, value):
## Note: Calling QWidget.itemChange causes segv in python 3 + PyQt
##ret = QtGui.QGraphicsItem.itemChange(self, change, value)
#ret = GraphicsWidget.itemChange(self, change, value)
#if change == self.ItemSceneChange:
#scene = self.scene()
#if scene is not None and hasattr(scene, 'sigPrepareForPaint'):
#scene.sigPrepareForPaint.disconnect(self.prepareForPaint)
#elif change == self.ItemSceneHasChanged:
#scene = self.scene()
#if scene is not None and hasattr(scene, 'sigPrepareForPaint'):
#scene.sigPrepareForPaint.connect(self.prepareForPaint)
#return ret
def checkSceneChange(self):
# ViewBox needs to receive sigPrepareForPaint from its scene before
# being painted. However, we have no way of being informed when the
# scene has changed in order to make this connection. The usual way
# to do this is via itemChange(), but bugs prevent this approach
# (see above). Instead, we simply check at every paint to see whether
# (the scene has changed.
scene = self.scene()
if scene == self._lastScene:
return
if self._lastScene is not None and hasattr(self.lastScene, 'sigPrepareForPaint'):
self._lastScene.sigPrepareForPaint.disconnect(self.prepareForPaint)
if scene is not None and hasattr(scene, 'sigPrepareForPaint'):
scene.sigPrepareForPaint.connect(self.prepareForPaint)
self.prepareForPaint()
self._lastScene = scene
def prepareForPaint(self):
#autoRangeEnabled = (self.state['autoRange'][0] is not False) or (self.state['autoRange'][1] is not False)
# don't check whether auto range is enabled here--only check when setting dirty flag.
if self._autoRangeNeedsUpdate: # and autoRangeEnabled:
self.updateAutoRange()
if self._matrixNeedsUpdate:
self.updateMatrix()
def getState(self, copy=True):
"""Return the current state of the ViewBox.
@ -214,7 +272,8 @@ class ViewBox(GraphicsWidget):
del state['linkedViews']
self.state.update(state)
self.updateMatrix()
#self.updateMatrix()
self.updateViewRange()
self.sigStateChanged.emit(self)
@ -274,6 +333,9 @@ class ViewBox(GraphicsWidget):
"""
if item.zValue() < self.zValue():
item.setZValue(self.zValue()+1)
scene = self.scene()
if scene is not None and scene is not item.scene():
scene.addItem(item) ## Necessary due to Qt bug: https://bugreports.qt-project.org/browse/QTBUG-18616
item.setParentItem(self.childGroup)
if not ignoreBounds:
self.addedItems.append(item)
@ -293,17 +355,17 @@ class ViewBox(GraphicsWidget):
for i in self.addedItems[:]:
self.removeItem(i)
for ch in self.childGroup.childItems():
ch.setParent(None)
ch.setParentItem(None)
def resizeEvent(self, ev):
#self.setRange(self.range, padding=0)
self.linkedXChanged()
self.linkedYChanged()
self.updateAutoRange()
self.updateMatrix()
self.updateViewRange()
self.sigStateChanged.emit(self)
self.background.setRect(self.rect())
#self._itemBoundsCache.clear()
#self.linkedXChanged()
#self.linkedYChanged()
self.sigResized.emit(self)
def viewRange(self):
"""Return a the view's visible range as a list: [[xmin, xmax], [ymin, ymax]]"""
@ -339,83 +401,128 @@ class ViewBox(GraphicsWidget):
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 *range*, *xRange*, or *yRange*.
Must specify at least one of *rect*, *xRange*, or *yRange*.
============= =====================================================================
================== =====================================================================
**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.
*padding* (float) Expand the view by a fraction of the requested range.
By default, this value is set between 0.02 and 0.1 depending on
the size of the ViewBox.
============= =====================================================================
*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.
*padding* (float) Expand the view by a fraction of the requested range.
By default, this value is set between 0.02 and 0.1 depending on
the size of the ViewBox.
*update* (bool) If True, update the range of the ViewBox immediately.
Otherwise, the update is deferred until before the next render.
*disableAutoRange* (bool) If True, auto-ranging is diabled. Otherwise, it is left
unchanged.
================== =====================================================================
"""
#print self.name, "ViewBox.setRange", rect, xRange, yRange, padding
#import traceback
#traceback.print_stack()
changes = {}
changes = {} # axes
setRequested = [False, False]
if rect is not None:
changes = {0: [rect.left(), rect.right()], 1: [rect.top(), rect.bottom()]}
setRequested = [True, True]
if xRange is not None:
changes[0] = xRange
setRequested[0] = True
if yRange is not None:
changes[1] = yRange
setRequested[1] = True
if len(changes) == 0:
print(rect)
raise Exception("Must specify at least one of rect, xRange, or yRange. (gave rect=%s)" % str(type(rect)))
# Update axes one at a time
changed = [False, False]
for ax, range in changes.items():
if padding is None:
xpad = self.suggestPadding(ax)
else:
xpad = padding
mn = min(range)
mx = max(range)
if mn == mx: ## If we requested 0 range, try to preserve previous scale. Otherwise just pick an arbitrary scale.
# If we requested 0 range, try to preserve previous scale.
# Otherwise just pick an arbitrary scale.
if mn == mx:
dy = self.state['viewRange'][ax][1] - self.state['viewRange'][ax][0]
if dy == 0:
dy = 1
mn -= dy*0.5
mx += dy*0.5
xpad = 0.0
if any(np.isnan([mn, mx])) or any(np.isinf([mn, mx])):
raise Exception("Not setting range [%s, %s]" % (str(mn), str(mx)))
# Make sure no nan/inf get through
if not all(np.isfinite([mn, mx])):
raise Exception("Cannot set range [%s, %s]" % (str(mn), str(mx)))
# Apply padding
if padding is None:
xpad = self.suggestPadding(ax)
else:
xpad = padding
p = (mx-mn) * xpad
mn -= p
mx += p
# Set target range
if self.state['targetRange'][ax] != [mn, mx]:
self.state['targetRange'][ax] = [mn, mx]
changed[ax] = True
if any(changed) and disableAutoRange:
if all(changed):
ax = ViewBox.XYAxes
elif changed[0]:
ax = ViewBox.XAxis
elif changed[1]:
ax = ViewBox.YAxis
self.enableAutoRange(ax, False)
self.sigStateChanged.emit(self)
if update:
self.updateMatrix(changed)
# Update viewRange to match targetRange as closely as possible while
# accounting for aspect ratio constraint
lockX, lockY = setRequested
if lockX and lockY:
lockX = False
lockY = False
self.updateViewRange(lockX, lockY)
for ax, range in changes.items():
link = self.linkedView(ax)
if link is not None:
link.linkedViewChanged(self, ax)
# Disable auto-range for each axis that was requested to be set
if disableAutoRange:
xOff = False if setRequested[0] else None
yOff = False if setRequested[1] else None
self.enableAutoRange(x=xOff, y=yOff)
changed.append(True)
# If nothing has changed, we are done.
if any(changed):
#if update and self.matrixNeedsUpdate:
#self.updateMatrix(changed)
#return
self.sigStateChanged.emit(self)
# Update target rect for debugging
if self.target.isVisible():
self.target.setRect(self.mapRectFromItem(self.childGroup, self.targetRect()))
# If ortho axes have auto-visible-only, update them now
# Note that aspect ratio constraints and auto-visible probably do not work together..
if changed[0] and self.state['autoVisibleOnly'][1] and (self.state['autoRange'][0] is not False):
self._autoRangeNeedsUpdate = True
#self.updateAutoRange() ## Maybe just indicate that auto range needs to be updated?
elif changed[1] and self.state['autoVisibleOnly'][0] and (self.state['autoRange'][1] is not False):
self._autoRangeNeedsUpdate = True
#self.updateAutoRange()
## Update view matrix only if requested
#if update:
#self.updateMatrix(changed)
## Otherwise, indicate that the matrix needs to be updated
#else:
#self.matrixNeedsUpdate = True
## Inform linked views that the range has changed <<This should be moved>>
#for ax, range in changes.items():
#link = self.linkedView(ax)
#if link is not None:
#link.linkedViewChanged(self, ax)
if changed[0] and self.state['autoVisibleOnly'][1]:
self.updateAutoRange()
elif changed[1] and self.state['autoVisibleOnly'][0]:
self.updateAutoRange()
def setYRange(self, min, max, padding=None, update=True):
"""
@ -465,37 +572,73 @@ class ViewBox(GraphicsWidget):
padding = 0.02
return padding
def scaleBy(self, s, center=None):
def scaleBy(self, s=None, center=None, x=None, y=None):
"""
Scale by *s* around given center point (or center of view).
*s* may be a Point or tuple (x, y)
*s* may be a Point or tuple (x, y).
Optionally, x or y may be specified individually. This allows the other
axis to be left unaffected (note that using a scale factor of 1.0 may
cause slight changes due to floating-point error).
"""
scale = Point(s)
if s is not None:
scale = Point(s)
else:
scale = [x, y]
affect = [True, True]
if scale[0] is None and scale[1] is None:
return
elif scale[0] is None:
affect[0] = False
scale[0] = 1.0
elif scale[1] is None:
affect[1] = False
scale[1] = 1.0
scale = Point(scale)
if self.state['aspectLocked'] is not False:
scale[0] = self.state['aspectLocked'] * scale[1]
scale[0] = scale[1]
vr = self.targetRect()
if center is None:
center = Point(vr.center())
else:
center = Point(center)
tl = center + (vr.topLeft()-center) * scale
br = center + (vr.bottomRight()-center) * scale
self.setRange(QtCore.QRectF(tl, br), padding=0)
def translateBy(self, t):
if not affect[0]:
self.setYRange(tl.y(), br.y(), padding=0)
elif not affect[1]:
self.setXRange(tl.x(), br.x(), padding=0)
else:
self.setRange(QtCore.QRectF(tl, br), padding=0)
def translateBy(self, t=None, x=None, y=None):
"""
Translate the view by *t*, which may be a Point or tuple (x, y).
Alternately, x or y may be specified independently, leaving the other
axis unchanged (note that using a translation of 0 may still cause
small changes due to floating-point error).
"""
t = Point(t)
#if viewCoords: ## scale from pixels
#o = self.mapToView(Point(0,0))
#t = self.mapToView(t) - o
vr = self.targetRect()
self.setRange(vr.translated(t), padding=0)
if t is not None:
t = Point(t)
self.setRange(vr.translated(t), padding=0)
else:
if x is not None:
x = vr.left()+x, vr.right()+x
if y is not None:
y = vr.top()+y, vr.bottom()+y
self.setRange(xRange=x, yRange=y, padding=0)
def enableAutoRange(self, axis=None, enable=True):
def enableAutoRange(self, axis=None, enable=True, x=None, y=None):
"""
Enable (or disable) auto-range for *axis*, which may be ViewBox.XAxis, ViewBox.YAxis, or ViewBox.XYAxes for both
(if *axis* is omitted, both axes will be changed).
@ -507,25 +650,47 @@ class ViewBox(GraphicsWidget):
#if not enable:
#import traceback
#traceback.print_stack()
# support simpler interface:
if x is not None or y is not None:
if x is not None:
self.enableAutoRange(ViewBox.XAxis, x)
if y is not None:
self.enableAutoRange(ViewBox.YAxis, y)
return
if enable is True:
enable = 1.0
if axis is None:
axis = ViewBox.XYAxes
needAutoRangeUpdate = False
if axis == ViewBox.XYAxes or axis == 'xy':
self.state['autoRange'][0] = enable
self.state['autoRange'][1] = enable
axes = [0, 1]
elif axis == ViewBox.XAxis or axis == 'x':
self.state['autoRange'][0] = enable
axes = [0]
elif axis == ViewBox.YAxis or axis == 'y':
self.state['autoRange'][1] = enable
axes = [1]
else:
raise Exception('axis argument must be ViewBox.XAxis, ViewBox.YAxis, or ViewBox.XYAxes.')
if enable:
self.updateAutoRange()
for ax in axes:
if self.state['autoRange'][ax] != enable:
# If we are disabling, do one last auto-range to make sure that
# previously scheduled auto-range changes are enacted
if enable is False and self._autoRangeNeedsUpdate:
self.updateAutoRange()
self.state['autoRange'][ax] = enable
self._autoRangeNeedsUpdate |= (enable is not False)
self.update()
#if needAutoRangeUpdate:
# self.updateAutoRange()
self.sigStateChanged.emit(self)
def disableAutoRange(self, axis=None):
@ -613,6 +778,7 @@ class ViewBox(GraphicsWidget):
args['disableAutoRange'] = False
self.setRange(**args)
finally:
self._autoRangeNeedsUpdate = False
self._updatingRange = False
def setXLink(self, view):
@ -651,6 +817,7 @@ class ViewBox(GraphicsWidget):
if oldLink is not None:
try:
getattr(oldLink, signal).disconnect(slot)
oldLink.sigResized.disconnect(slot)
except TypeError:
## This can occur if the view has been deleted already
pass
@ -661,12 +828,14 @@ class ViewBox(GraphicsWidget):
else:
self.state['linkedViews'][axis] = weakref.ref(view)
getattr(view, signal).connect(slot)
view.sigResized.connect(slot)
if view.autoRangeEnabled()[axis] is not False:
self.enableAutoRange(axis, False)
slot()
else:
if self.autoRangeEnabled()[axis] is False:
slot()
self.sigStateChanged.emit(self)
@ -696,6 +865,7 @@ class ViewBox(GraphicsWidget):
if self.linksBlocked or view is None:
return
#print self.name, "ViewBox.linkedViewChanged", axis, view.viewRange()[axis]
vr = view.viewRect()
vg = view.screenGeometry()
sg = self.screenGeometry()
@ -719,12 +889,15 @@ class ViewBox(GraphicsWidget):
else:
overlap = min(sg.bottom(), vg.bottom()) - max(sg.top(), vg.top())
if overlap < min(vg.height()/3, sg.height()/3): ## if less than 1/3 of views overlap,
## then just replicate the view
## then just replicate the view
y1 = vr.top()
y2 = vr.bottom()
else: ## views overlap; line them up
upp = float(vr.height()) / vg.height()
y2 = vr.bottom() - (sg.y()-vg.y()) * upp
if self.yInverted():
y2 = vr.bottom() + (sg.bottom()-vg.bottom()) * upp
else:
y2 = vr.bottom() + (sg.top()-vg.top()) * upp
y1 = y2 - sg.height() * upp
self.enableAutoRange(ViewBox.YAxis, False)
self.setYRange(y1, y2, padding=0)
@ -751,14 +924,21 @@ class ViewBox(GraphicsWidget):
def itemBoundsChanged(self, item):
self._itemBoundsCache.pop(item, None)
self.updateAutoRange()
if (self.state['autoRange'][0] is not False) or (self.state['autoRange'][1] is not False):
self._autoRangeNeedsUpdate = True
self.update()
#self.updateAutoRange()
def invertY(self, b=True):
"""
By default, the positive y-axis points upward on the screen. Use invertY(True) to reverse the y-axis.
"""
if self.state['yInverted'] == b:
return
self.state['yInverted'] = b
self.updateMatrix(changed=(False, True))
#self.updateMatrix(changed=(False, True))
self.updateViewRange()
self.sigStateChanged.emit(self)
def yInverted(self):
@ -768,19 +948,31 @@ class ViewBox(GraphicsWidget):
"""
If the aspect ratio is locked, view scaling must always preserve the aspect ratio.
By default, the ratio is set to 1; x and y both have the same scaling.
This ratio can be overridden (width/height), or use None to lock in the current ratio.
This ratio can be overridden (xScale/yScale), or use None to lock in the current ratio.
"""
if not lock:
if self.state['aspectLocked'] == False:
return
self.state['aspectLocked'] = False
else:
rect = self.rect()
vr = self.viewRect()
currentRatio = vr.width() / vr.height()
if rect.height() == 0 or vr.width() == 0 or vr.height() == 0:
currentRatio = 1.0
else:
currentRatio = (rect.width()/float(rect.height())) / (vr.width()/vr.height())
if ratio is None:
ratio = currentRatio
if self.state['aspectLocked'] == ratio: # nothing to change
return
self.state['aspectLocked'] = ratio
if ratio != currentRatio: ## If this would change the current range, do that now
#self.setRange(0, self.state['viewRange'][0][0], self.state['viewRange'][0][1])
self.updateMatrix()
self.updateViewRange()
self.updateAutoRange()
self.updateViewRange()
self.sigStateChanged.emit(self)
def childTransform(self):
@ -911,7 +1103,8 @@ class ViewBox(GraphicsWidget):
dif = dif * -1
## Ignore axes if mouse is disabled
mask = np.array(self.state['mouseEnabled'], dtype=np.float)
mouseEnabled = np.array(self.state['mouseEnabled'], dtype=np.float)
mask = mouseEnabled.copy()
if axis is not None:
mask[1-axis] = 0.0
@ -933,7 +1126,10 @@ class ViewBox(GraphicsWidget):
else:
tr = dif*mask
tr = self.mapToView(tr) - self.mapToView(Point(0,0))
self.translateBy(tr)
x = tr.x() if mask[0] == 1 else None
y = tr.y() if mask[1] == 1 else None
self.translateBy(x=x, y=y)
self.sigRangeChangedManually.emit(self.state['mouseEnabled'])
elif ev.button() & QtCore.Qt.RightButton:
#print "vb.rightDrag"
@ -948,8 +1144,11 @@ class ViewBox(GraphicsWidget):
tr = self.childGroup.transform()
tr = fn.invertQTransform(tr)
x = s[0] if mouseEnabled[0] == 1 else None
y = s[1] if mouseEnabled[1] == 1 else None
center = Point(tr.map(ev.buttonDownPos(QtCore.Qt.RightButton)))
self.scaleBy(s, center)
self.scaleBy(x=x, y=y, center=center)
self.sigRangeChangedManually.emit(self.state['mouseEnabled'])
def keyPressEvent(self, ev):
@ -1046,10 +1245,10 @@ class ViewBox(GraphicsWidget):
xr = item.dataBounds(0, frac=frac[0], orthoRange=orthoRange[0])
yr = item.dataBounds(1, frac=frac[1], orthoRange=orthoRange[1])
pxPad = 0 if not hasattr(item, 'pixelPadding') else item.pixelPadding()
if xr is None or xr == (None, None):
if xr is None or (xr[0] is None and xr[1] is None) or np.isnan(xr).any() or np.isinf(xr).any():
useX = False
xr = (0,0)
if yr is None or yr == (None, None):
if yr is None or (yr[0] is None and yr[1] is None) or np.isnan(yr).any() or np.isinf(yr).any():
useY = False
yr = (0,0)
@ -1140,40 +1339,79 @@ class ViewBox(GraphicsWidget):
bounds = QtCore.QRectF(range[0][0], range[1][0], range[0][1]-range[0][0], range[1][1]-range[1][0])
return bounds
def updateViewRange(self, forceX=False, forceY=False):
## Update viewRange to match targetRange as closely as possible, given
## aspect ratio constraints. The *force* arguments are used to indicate
## which axis (if any) should be unchanged when applying constraints.
viewRange = [self.state['targetRange'][0][:], self.state['targetRange'][1][:]]
changed = [False, False]
def updateMatrix(self, changed=None):
## Make the childGroup's transform match the requested range.
# Make correction for aspect ratio constraint
if changed is None:
changed = [False, False]
changed = list(changed)
#print "udpateMatrix:"
#print " range:", self.range
## aspect is (widget w/h) / (view range w/h)
aspect = self.state['aspectLocked'] # size ratio / view ratio
tr = self.targetRect()
bounds = self.rect() #boundingRect()
#print bounds
## set viewRect, given targetRect and possibly aspect ratio constraint
if self.state['aspectLocked'] is False or bounds.height() == 0:
self.state['viewRange'] = [self.state['targetRange'][0][:], self.state['targetRange'][1][:]]
else:
viewRatio = bounds.width() / bounds.height()
targetRatio = self.state['aspectLocked'] * tr.width() / tr.height()
if targetRatio > viewRatio:
## target is wider than view
dy = 0.5 * (tr.width() / (self.state['aspectLocked'] * viewRatio) - tr.height())
bounds = self.rect()
if aspect is not False and aspect != 0 and tr.height() != 0 and bounds.height() != 0:
## This is the view range aspect ratio we have requested
targetRatio = tr.width() / tr.height()
## This is the view range aspect ratio we need to obey aspect constraint
viewRatio = (bounds.width() / bounds.height()) / aspect
# Decide which range to keep unchanged
#print self.name, "aspect:", aspect, "changed:", changed, "auto:", self.state['autoRange']
if forceX:
ax = 0
elif forceY:
ax = 1
else:
# if we are not required to keep a particular axis unchanged,
# 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())
if dy != 0:
changed[1] = True
self.state['viewRange'] = [self.state['targetRange'][0][:], [self.state['targetRange'][1][0] - dy, self.state['targetRange'][1][1] + dy]]
viewRange[1] = [self.state['targetRange'][1][0] - dy, self.state['targetRange'][1][1] + dy]
else:
dx = 0.5 * (tr.height() * viewRatio * self.state['aspectLocked'] - tr.width())
## view range needs to be wider than target
dx = 0.5 * (tr.height() * viewRatio - tr.width())
if dx != 0:
changed[0] = True
self.state['viewRange'] = [[self.state['targetRange'][0][0] - dx, self.state['targetRange'][0][1] + dx], self.state['targetRange'][1][:]]
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)]
self.state['viewRange'] = viewRange
# emit range change signals
if changed[0]:
self.sigXRangeChanged.emit(self, tuple(self.state['viewRange'][0]))
if changed[1]:
self.sigYRangeChanged.emit(self, tuple(self.state['viewRange'][1]))
if any(changed):
self.sigRangeChanged.emit(self, self.state['viewRange'])
self.update()
# Inform linked views that the range has changed
for ax in [0, 1]:
if not changed[ax]:
continue
link = self.linkedView(ax)
if link is not None:
link.linkedViewChanged(self, ax)
self._matrixNeedsUpdate = True
def updateMatrix(self, changed=None):
## Make the childGroup's transform match the requested viewRange.
bounds = self.rect()
vr = self.viewRect()
#print " bounds:", bounds
if vr.height() == 0 or vr.width() == 0:
return
scale = Point(bounds.width()/vr.width(), bounds.height()/vr.height())
@ -1192,21 +1430,23 @@ class ViewBox(GraphicsWidget):
self.childGroup.setTransform(m)
if changed[0]:
self.sigXRangeChanged.emit(self, tuple(self.state['viewRange'][0]))
if changed[1]:
self.sigYRangeChanged.emit(self, tuple(self.state['viewRange'][1]))
if any(changed):
self.sigRangeChanged.emit(self, self.state['viewRange'])
self.sigTransformChanged.emit(self) ## segfaults here: 1
self._matrixNeedsUpdate = False
def paint(self, p, opt, widget):
self.checkSceneChange()
if self.border is not None:
bounds = self.shape()
p.setPen(self.border)
#p.fillRect(bounds, QtGui.QColor(0, 0, 0))
p.drawPath(bounds)
#p.setPen(fn.mkPen('r'))
#path = QtGui.QPainterPath()
#path.addRect(self.targetRect())
#tr = self.mapFromView(path)
#p.drawPath(tr)
def updateBackground(self):
bg = self.state['background']
@ -1276,6 +1516,8 @@ class ViewBox(GraphicsWidget):
k.destroyed.disconnect()
except RuntimeError: ## signal is already disconnected.
pass
except TypeError: ## view has already been deleted (?)
pass
def locate(self, item, timeout=3.0, children=False):
"""

View File

@ -65,8 +65,18 @@ class ViewBoxMenu(QtGui.QMenu):
self.leftMenu = QtGui.QMenu("Mouse Mode")
group = QtGui.QActionGroup(self)
pan = self.leftMenu.addAction("3 button", self.set3ButtonMode)
zoom = self.leftMenu.addAction("1 button", self.set1ButtonMode)
# This does not work! QAction _must_ be initialized with a permanent
# object as the parent or else it may be collected prematurely.
#pan = self.leftMenu.addAction("3 button", self.set3ButtonMode)
#zoom = self.leftMenu.addAction("1 button", self.set1ButtonMode)
pan = QtGui.QAction("3 button", self.leftMenu)
zoom = QtGui.QAction("1 button", self.leftMenu)
self.leftMenu.addAction(pan)
self.leftMenu.addAction(zoom)
pan.triggered.connect(self.set3ButtonMode)
zoom.triggered.connect(self.set1ButtonMode)
pan.setCheckable(True)
zoom.setCheckable(True)
pan.setActionGroup(group)

View File

@ -0,0 +1,95 @@
"""
ViewBox test cases:
* call setRange then resize; requested range must be fully visible
* lockAspect works correctly for arbitrary aspect ratio
* autoRange works correctly with aspect locked
* call setRange with aspect locked, then resize
* AutoRange with all the bells and whistles
* item moves / changes transformation / changes bounds
* pan only
* fractional range
"""
import pyqtgraph as pg
app = pg.mkQApp()
imgData = pg.np.zeros((10, 10))
imgData[0] = 3
imgData[-1] = 3
imgData[:,0] = 3
imgData[:,-1] = 3
def testLinkWithAspectLock():
global win, vb
win = pg.GraphicsWindow()
vb = win.addViewBox(name="image view")
vb.setAspectLocked()
vb.enableAutoRange(x=False, y=False)
p1 = win.addPlot(name="plot 1")
p2 = win.addPlot(name="plot 2", row=1, col=0)
win.ci.layout.setRowFixedHeight(1, 150)
win.ci.layout.setColumnFixedWidth(1, 150)
def viewsMatch():
r0 = pg.np.array(vb.viewRange())
r1 = pg.np.array(p1.vb.viewRange()[1])
r2 = pg.np.array(p2.vb.viewRange()[1])
match = (abs(r0[1]-r1) <= (abs(r1) * 0.001)).all() and (abs(r0[0]-r2) <= (abs(r2) * 0.001)).all()
return match
p1.setYLink(vb)
p2.setXLink(vb)
print "link views match:", viewsMatch()
win.show()
print "show views match:", viewsMatch()
img = pg.ImageItem(imgData)
vb.addItem(img)
vb.autoRange()
p1.plot(x=imgData.sum(axis=0), y=range(10))
p2.plot(x=range(10), y=imgData.sum(axis=1))
print "add items views match:", viewsMatch()
#p1.setAspectLocked()
#grid = pg.GridItem()
#vb.addItem(grid)
pg.QtGui.QApplication.processEvents()
pg.QtGui.QApplication.processEvents()
#win.resize(801, 600)
def testAspectLock():
global win, vb
win = pg.GraphicsWindow()
vb = win.addViewBox(name="image view")
vb.setAspectLocked()
img = pg.ImageItem(imgData)
vb.addItem(img)
#app.processEvents()
#print "init views match:", viewsMatch()
#p2.setYRange(-300, 300)
#print "setRange views match:", viewsMatch()
#app.processEvents()
#print "setRange views match (after update):", viewsMatch()
#print "--lock aspect--"
#p1.setAspectLocked(True)
#print "lockAspect views match:", viewsMatch()
#p2.setYRange(-200, 200)
#print "setRange views match:", viewsMatch()
#app.processEvents()
#print "setRange views match (after update):", viewsMatch()
#win.resize(100, 600)
#app.processEvents()
#vb.setRange(xRange=[-10, 10], padding=0)
#app.processEvents()
#win.resize(600, 100)
#app.processEvents()
#print vb.viewRange()
if __name__ == '__main__':
testLinkWithAspectLock()

View File

@ -90,14 +90,6 @@ class ImageView(QtGui.QWidget):
self.ignoreTimeLine = False
#if 'linux' in sys.platform.lower(): ## Stupid GL bug in linux.
# self.ui.graphicsView.setViewport(QtGui.QWidget())
#self.ui.graphicsView.enableMouse(True)
#self.ui.graphicsView.autoPixelRange = False
#self.ui.graphicsView.setAspectLocked(True)
#self.ui.graphicsView.invertY()
#self.ui.graphicsView.enableMouse()
if view is None:
self.view = ViewBox()
else:
@ -106,13 +98,6 @@ class ImageView(QtGui.QWidget):
self.view.setAspectLocked(True)
self.view.invertY()
#self.ticks = [t[0] for t in self.ui.gradientWidget.listTicks()]
#self.ticks[0].colorChangeAllowed = False
#self.ticks[1].colorChangeAllowed = False
#self.ui.gradientWidget.allowAdd = False
#self.ui.gradientWidget.setTickColor(self.ticks[1], QtGui.QColor(255,255,255))
#self.ui.gradientWidget.setOrientation('right')
if imageItem is None:
self.imageItem = ImageItem()
else:
@ -133,7 +118,6 @@ class ImageView(QtGui.QWidget):
self.normRoi.setZValue(20)
self.view.addItem(self.normRoi)
self.normRoi.hide()
#self.ui.roiPlot.hide()
self.roiCurve = self.ui.roiPlot.plot()
self.timeLine = InfiniteLine(0, movable=True)
self.timeLine.setPen(QtGui.QPen(QtGui.QColor(255, 255, 0, 200)))
@ -147,13 +131,6 @@ class ImageView(QtGui.QWidget):
self.playRate = 0
self.lastPlayTime = 0
#self.normLines = []
#for i in [0,1]:
#l = InfiniteLine(self.ui.roiPlot, 0)
#l.setPen(QtGui.QPen(QtGui.QColor(0, 100, 200, 200)))
#self.ui.roiPlot.addItem(l)
#self.normLines.append(l)
#l.hide()
self.normRgn = LinearRegionItem()
self.normRgn.setZValue(0)
self.ui.roiPlot.addItem(self.normRgn)
@ -168,7 +145,6 @@ class ImageView(QtGui.QWidget):
setattr(self, fn, getattr(self.ui.histogram, fn))
self.timeLine.sigPositionChanged.connect(self.timeLineChanged)
#self.ui.gradientWidget.sigGradientChanged.connect(self.updateImage)
self.ui.roiBtn.clicked.connect(self.roiClicked)
self.roi.sigRegionChanged.connect(self.roiChanged)
self.ui.normBtn.toggled.connect(self.normToggled)
@ -187,31 +163,32 @@ class ImageView(QtGui.QWidget):
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.roiClicked() ## initialize roi plot to correct shape / visibility
def setImage(self, img, autoRange=True, autoLevels=True, levels=None, axes=None, xvals=None, pos=None, scale=None, transform=None):
def setImage(self, img, autoRange=True, autoLevels=True, levels=None, axes=None, xvals=None, pos=None, scale=None, transform=None, autoHistogramRange=True):
"""
Set the image to be displayed in the widget.
============== =======================================================================
================== =======================================================================
**Arguments:**
*img* (numpy array) the image to be displayed.
*xvals* (numpy array) 1D array of z-axis values corresponding to the third axis
in a 3D image. For video, this array should contain the time of each frame.
*autoRange* (bool) whether to scale/pan the view to fit the image.
*autoLevels* (bool) whether to update the white/black levels to fit the image.
*levels* (min, max); the white and black level values to use.
*axes* Dictionary indicating the interpretation for each axis.
This is only needed to override the default guess. Format is::
img (numpy array) the image to be displayed.
xvals (numpy array) 1D array of z-axis values corresponding to the third axis
in a 3D image. For video, this array should contain the time of each frame.
autoRange (bool) whether to scale/pan the view to fit the image.
autoLevels (bool) whether to update the white/black levels to fit the image.
levels (min, max); the white and black level values to use.
axes Dictionary indicating the interpretation for each axis.
This is only needed to override the default guess. Format is::
{'t':0, 'x':1, 'y':2, 'c':3};
{'t':0, 'x':1, 'y':2, 'c':3};
*pos* Change the position of the displayed image
*scale* Change the scale of the displayed image
*transform* Set the transform of the dispalyed image. This option overrides *pos*
and *scale*.
============== =======================================================================
pos Change the position of the displayed image
scale Change the scale of the displayed image
transform Set the transform of the displayed image. This option overrides *pos*
and *scale*.
autoHistogramRange If True, the histogram y-range is automatically scaled to fit the
image data.
================== =======================================================================
"""
prof = debug.Profiler('ImageView.setImage', disabled=True)
@ -231,9 +208,7 @@ class ImageView(QtGui.QWidget):
self.tVals = np.arange(img.shape[0])
else:
self.tVals = np.arange(img.shape[0])
#self.ui.timeSlider.setValue(0)
#self.ui.normStartSlider.setValue(0)
#self.ui.timeSlider.setMaximum(img.shape[0]-1)
prof.mark('1')
if axes is None:
@ -265,14 +240,13 @@ class ImageView(QtGui.QWidget):
prof.mark('3')
self.currentIndex = 0
self.updateImage()
self.updateImage(autoHistogramRange=autoHistogramRange)
if levels is None and autoLevels:
self.autoLevels()
if levels is not None: ## this does nothing since getProcessedImage sets these values again.
self.levelMax = levels[1]
self.levelMin = levels[0]
self.setLevels(*levels)
if self.ui.roiBtn.isChecked():
self.roiChanged()
@ -328,15 +302,9 @@ class ImageView(QtGui.QWidget):
if not self.playTimer.isActive():
self.playTimer.start(16)
def autoLevels(self):
"""Set the min/max levels automatically to match the image data."""
#image = self.getProcessedImage()
"""Set the min/max intensity levels automatically to match the image data."""
self.setLevels(self.levelMin, self.levelMax)
#self.ui.histogram.imageChanged(autoLevel=True)
def setLevels(self, min, max):
"""Set the min/max (bright and dark) levels."""
@ -345,17 +313,16 @@ class ImageView(QtGui.QWidget):
def autoRange(self):
"""Auto scale and pan the view around the image."""
image = self.getProcessedImage()
#self.ui.graphicsView.setRange(QtCore.QRectF(0, 0, image.shape[self.axes['x']], image.shape[self.axes['y']]), padding=0., lockAspect=True)
self.view.autoRange() ##setRange(self.imageItem.viewBoundingRect(), padding=0.)
self.view.autoRange()
def getProcessedImage(self):
"""Returns the image data after it has been processed by any normalization options in use."""
"""Returns the image data after it has been processed by any normalization options in use.
This method also sets the attributes self.levelMin and self.levelMax
to indicate the range of data in the image."""
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.ui.histogram.setHistogramRange(self.levelMin, self.levelMax)
return self.imageDisp
@ -364,7 +331,6 @@ class ImageView(QtGui.QWidget):
"""Closes the widget nicely, making sure to clear the graphics scene and release memory."""
self.ui.roiPlot.close()
self.ui.graphicsView.close()
#self.ui.gradientWidget.sigGradientChanged.disconnect(self.updateImage)
self.scene.clear()
del self.image
del self.imageDisp
@ -467,20 +433,12 @@ class ImageView(QtGui.QWidget):
def normRadioChanged(self):
self.imageDisp = None
self.updateImage()
self.autoLevels()
self.roiChanged()
self.sigProcessingChanged.emit(self)
def updateNorm(self):
#for l, sl in zip(self.normLines, [self.ui.normStartSlider, self.ui.normStopSlider]):
#if self.ui.normTimeRangeCheck.isChecked():
#l.show()
#else:
#l.hide()
#i, t = self.timeIndex(sl)
#l.setPos(t)
if self.ui.normTimeRangeCheck.isChecked():
#print "show!"
self.normRgn.show()
@ -496,6 +454,7 @@ class ImageView(QtGui.QWidget):
if not self.ui.normOffRadio.isChecked():
self.imageDisp = None
self.updateImage()
self.autoLevels()
self.roiChanged()
self.sigProcessingChanged.emit(self)
@ -633,22 +592,19 @@ class ImageView(QtGui.QWidget):
#self.emit(QtCore.SIGNAL('timeChanged'), ind, time)
self.sigTimeChanged.emit(ind, time)
def updateImage(self):
def updateImage(self, autoHistogramRange=True):
## Redraw image on screen
if self.image is None:
return
image = self.getProcessedImage()
#print "update:", image.ndim, image.max(), image.min(), self.blackLevel(), self.whiteLevel()
if autoHistogramRange:
self.ui.histogram.setHistogramRange(self.levelMin, self.levelMax)
if self.axes['t'] is None:
#self.ui.timeSlider.hide()
self.imageItem.updateImage(image)
#self.ui.roiPlot.hide()
#self.ui.roiBtn.hide()
else:
#self.ui.roiBtn.show()
self.ui.roiPlot.show()
#self.ui.timeSlider.show()
self.imageItem.updateImage(image[self.currentIndex])
@ -656,38 +612,22 @@ class ImageView(QtGui.QWidget):
## Return the time and frame index indicated by a slider
if self.image is None:
return (0,0)
#v = slider.value()
#vmax = slider.maximum()
#f = float(v) / vmax
t = slider.value()
#t = 0.0
#xv = self.image.xvals('Time')
xv = self.tVals
if xv is None:
ind = int(t)
#ind = int(f * self.image.shape[0])
else:
if len(xv) < 2:
return (0,0)
totTime = xv[-1] + (xv[-1]-xv[-2])
#t = f * totTime
inds = np.argwhere(xv < t)
if len(inds) < 1:
return (0,t)
ind = inds[-1,0]
#print ind
return ind, t
#def whiteLevel(self):
#return self.levelMin + (self.levelMax-self.levelMin) * self.ui.gradientWidget.tickValue(self.ticks[1])
##return self.levelMin + (self.levelMax-self.levelMin) * self.ui.whiteSlider.value() / self.ui.whiteSlider.maximum()
#def blackLevel(self):
#return self.levelMin + (self.levelMax-self.levelMin) * self.ui.gradientWidget.tickValue(self.ticks[0])
##return self.levelMin + ((self.levelMax-self.levelMin) / self.ui.blackSlider.maximum()) * self.ui.blackSlider.value()
def getView(self):
"""Return the ViewBox (or other compatible object) which displays the ImageItem"""
return self.view

View File

@ -328,6 +328,9 @@ class MetaArray(object):
def __div__(self, b):
return self._binop('__div__', b)
def __truediv__(self, b):
return self._binop('__truediv__', b)
def _binop(self, op, b):
if isinstance(b, MetaArray):
b = b.asarray()

View File

@ -20,10 +20,9 @@ if __name__ == '__main__':
if opts.pop('pyside', False):
import PySide
#import pyqtgraph
#import pyqtgraph.multiprocess.processes
targetStr = opts.pop('targetStr')
target = pickle.loads(targetStr) ## unpickling the target should import everything we need
#target(name, port, authkey, ppid)
target(**opts) ## Send all other options to the target function
sys.exit(0)

View File

@ -129,7 +129,7 @@ class Parallelize(object):
self.childs.append(proc)
## Keep track of the progress of each worker independently.
self.progress = {ch.childPid: [] for ch in self.childs}
self.progress = dict([(ch.childPid, []) for ch in self.childs])
## for each child process, self.progress[pid] is a list
## of task indexes. The last index is the task currently being
## processed; all others are finished.

View File

@ -1,7 +1,7 @@
from .remoteproxy import RemoteEventHandler, ClosedError, NoResultError, LocalObjectProxy, ObjectProxy
import subprocess, atexit, os, sys, time, random, socket, signal
import multiprocessing.connection
from pyqtgraph.Qt import USE_PYSIDE
import pyqtgraph as pg
try:
import cPickle as pickle
except ImportError:
@ -35,7 +35,7 @@ class Process(RemoteEventHandler):
ProxyObject for more information.
"""
def __init__(self, name=None, target=None, executable=None, copySysPath=True, debug=False):
def __init__(self, name=None, target=None, executable=None, copySysPath=True, debug=False, timeout=20, wrapStdout=None):
"""
============ =============================================================
Arguments:
@ -49,8 +49,13 @@ class Process(RemoteEventHandler):
copySysPath If True, copy the contents of sys.path to the remote process
debug If True, print detailed information about communication
with the child process.
wrapStdout If True (default on windows) then stdout and stderr from the
child process will be caught by the parent process and
forwarded to its stdout/stderr. This provides a workaround
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
@ -62,28 +67,48 @@ class Process(RemoteEventHandler):
## random authentication key
authkey = os.urandom(20)
## Windows seems to have a hard time with hmac
if sys.platform.startswith('win'):
authkey = None
#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:
## hmac authentication appears to be broken on windows (says AuthenticationError: digest received was wrong)
l = multiprocessing.connection.Listener(('localhost', int(port)), authkey=authkey)
break
except socket.error as ex:
if ex.errno != 98:
if ex.errno != 98 and ex.errno != 10048: # unix=98, win=10048
raise
port += 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))
self.proc = subprocess.Popen((executable, bootstrap), stdin=subprocess.PIPE)
if wrapStdout is None:
wrapStdout = sys.platform.startswith('win')
if wrapStdout:
## note: we need all three streams to have their own PIPE due to this bug:
## http://bugs.python.org/issue3905
stdout = subprocess.PIPE
stderr = subprocess.PIPE
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")
else:
self.proc = subprocess.Popen((executable, bootstrap), stdin=subprocess.PIPE)
targetStr = pickle.dumps(target) ## double-pickle target so that child has a chance to
## set its sys.path properly before unpickling the target
pid = os.getpid() # we must send pid to child because windows does not have getppid
pyside = USE_PYSIDE
## Send everything the remote process needs to start correctly
data = dict(
@ -93,14 +118,14 @@ class Process(RemoteEventHandler):
ppid=pid,
targetStr=targetStr,
path=sysPath,
pyside=pyside,
pyside=pg.Qt.USE_PYSIDE,
debug=debug
)
pickle.dump(data, self.proc.stdin)
self.proc.stdin.close()
## open connection for remote process
self.debugMsg('Listening for child process..')
self.debugMsg('Listening for child process on port %d, authkey=%s..' % (port, repr(authkey)))
while True:
try:
conn = l.accept()
@ -115,6 +140,7 @@ class Process(RemoteEventHandler):
self.debugMsg('Connected to child process.')
atexit.register(self.join)
def join(self, timeout=10):
self.debugMsg('Joining child process..')
@ -126,10 +152,24 @@ class Process(RemoteEventHandler):
raise Exception('Timed out waiting for remote process to end.')
time.sleep(0.05)
self.debugMsg('Child process exited. (%d)' % self.proc.returncode)
def debugMsg(self, msg):
if hasattr(self, '_stdoutForwarder'):
## Lock output from subprocess to make sure we do not get line collisions
with self._stdoutForwarder.lock:
with self._stderrForwarder.lock:
RemoteEventHandler.debugMsg(self, msg)
else:
RemoteEventHandler.debugMsg(self, msg)
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)))
conn = multiprocessing.connection.Client(('localhost', int(port)), authkey=authkey)
if debug:
print('[%d] connected; starting remote proxy.' % os.getpid())
global HANDLER
#ppid = 0 if not hasattr(os, 'getppid') else os.getppid()
HANDLER = RemoteEventHandler(conn, name, ppid, debug=debug)
@ -321,7 +361,8 @@ class QtProcess(Process):
GUI.
- A QTimer is also started on the parent process which polls for requests
from the child process. This allows Qt signals emitted within the child
process to invoke slots on the parent process and vice-versa.
process to invoke slots on the parent process and vice-versa. This can
be disabled using processRequests=False in the constructor.
Example::
@ -338,18 +379,29 @@ class QtProcess(Process):
def __init__(self, **kwds):
if 'target' not in kwds:
kwds['target'] = startQtEventLoop
self._processRequests = kwds.pop('processRequests', True)
Process.__init__(self, **kwds)
self.startEventTimer()
def startEventTimer(self):
from pyqtgraph.Qt import QtGui, QtCore ## avoid module-level import to keep bootstrap snappy.
self.timer = QtCore.QTimer()
app = QtGui.QApplication.instance()
if app is None:
raise Exception("Must create QApplication before starting QtProcess")
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):
"""Start listening for requests coming from the child process.
This allows signals to be connected from the child process to the parent.
"""
self.timer.timeout.connect(self.processRequests)
self.timer.start(10)
self.timer.start(interval*1000)
def stopRequestProcessing(self):
self.timer.stop()
def processRequests(self):
try:
Process.processRequests(self)
@ -357,7 +409,12 @@ class QtProcess(Process):
self.timer.stop()
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)))
conn = multiprocessing.connection.Client(('localhost', int(port)), authkey=authkey)
if debug:
print('[%d] connected; starting remote proxy.' % os.getpid())
from pyqtgraph.Qt import QtGui, QtCore
#from PyQt4 import QtGui, QtCore
app = QtGui.QApplication.instance()
@ -373,4 +430,43 @@ def startQtEventLoop(name, port, authkey, ppid, debug=False):
HANDLER.startEventTimer()
app.exec_()
import threading
class FileForwarder(threading.Thread):
"""
Background thread that forwards data from one pipe to another.
This is used to catch data from stdout/stderr of the child process
and print it back out to stdout/stderr. We need this because this
bug: http://bugs.python.org/issue3905 _requires_ us to catch
stdout/stderr.
*output* may be a file or 'stdout' or 'stderr'. In the latter cases,
sys.stdout/stderr are retrieved once for every line that is output,
which ensures that the correct behavior is achieved even if
sys.stdout/stderr are replaced at runtime.
"""
def __init__(self, input, output):
threading.Thread.__init__(self)
self.input = input
self.output = output
self.lock = threading.Lock()
self.start()
def run(self):
if self.output == 'stdout':
while True:
line = self.input.readline()
with self.lock:
sys.stdout.write(line)
elif self.output == 'stderr':
while True:
line = self.input.readline()
with self.lock:
sys.stderr.write(line)
else:
while True:
line = self.input.readline()
with self.lock:
self.output.write(line)

View File

@ -97,7 +97,6 @@ class RemoteEventHandler(object):
after no more events are immediately available. (non-blocking)
Returns the number of events processed.
"""
self.debugMsg('processRequests:')
if self.exited:
self.debugMsg(' processRequests: exited already; raise ClosedError.')
raise ClosedError()
@ -108,7 +107,7 @@ class RemoteEventHandler(object):
self.handleRequest()
numProcessed += 1
except ClosedError:
self.debugMsg(' processRequests: got ClosedError from handleRequest; setting exited=True.')
self.debugMsg('processRequests: got ClosedError from handleRequest; setting exited=True.')
self.exited = True
raise
#except IOError as err: ## let handleRequest take care of this.
@ -121,7 +120,8 @@ class RemoteEventHandler(object):
print("Error in process %s" % self.name)
sys.excepthook(*sys.exc_info())
self.debugMsg(' processRequests: finished %d requests' % numProcessed)
if numProcessed > 0:
self.debugMsg('processRequests: finished %d requests' % numProcessed)
return numProcessed
def handleRequest(self):
@ -205,7 +205,11 @@ class RemoteEventHandler(object):
fnkwds[k] = np.fromstring(byteData[ind], dtype=dtype).reshape(shape)
if len(fnkwds) == 0: ## need to do this because some functions do not allow keyword arguments.
result = obj(*fnargs)
try:
result = obj(*fnargs)
except:
print("Failed to call object %s: %d, %s" % (obj, len(fnargs), fnargs[1:]))
raise
else:
result = obj(*fnargs, **fnkwds)
@ -803,7 +807,7 @@ class ObjectProxy(object):
return val
def _getProxyOptions(self):
return {k: self._getProxyOption(k) for k in self._proxyOptions}
return dict([(k, self._getProxyOption(k)) for k in self._proxyOptions])
def __reduce__(self):
return (unpickleObjectProxy, (self._processId, self._proxyId, self._typeStr, self._attributes))
@ -887,6 +891,12 @@ class ObjectProxy(object):
def __div__(self, *args):
return self._getSpecialAttr('__div__')(*args)
def __truediv__(self, *args):
return self._getSpecialAttr('__truediv__')(*args)
def __floordiv__(self, *args):
return self._getSpecialAttr('__floordiv__')(*args)
def __mul__(self, *args):
return self._getSpecialAttr('__mul__')(*args)
@ -902,6 +912,12 @@ class ObjectProxy(object):
def __idiv__(self, *args):
return self._getSpecialAttr('__idiv__')(*args, _callSync='off')
def __itruediv__(self, *args):
return self._getSpecialAttr('__itruediv__')(*args, _callSync='off')
def __ifloordiv__(self, *args):
return self._getSpecialAttr('__ifloordiv__')(*args, _callSync='off')
def __imul__(self, *args):
return self._getSpecialAttr('__imul__')(*args, _callSync='off')
@ -914,17 +930,11 @@ class ObjectProxy(object):
def __lshift__(self, *args):
return self._getSpecialAttr('__lshift__')(*args)
def __floordiv__(self, *args):
return self._getSpecialAttr('__pow__')(*args)
def __irshift__(self, *args):
return self._getSpecialAttr('__rshift__')(*args, _callSync='off')
return self._getSpecialAttr('__irshift__')(*args, _callSync='off')
def __ilshift__(self, *args):
return self._getSpecialAttr('__lshift__')(*args, _callSync='off')
def __ifloordiv__(self, *args):
return self._getSpecialAttr('__pow__')(*args, _callSync='off')
return self._getSpecialAttr('__ilshift__')(*args, _callSync='off')
def __eq__(self, *args):
return self._getSpecialAttr('__eq__')(*args)
@ -974,6 +984,12 @@ class ObjectProxy(object):
def __rdiv__(self, *args):
return self._getSpecialAttr('__rdiv__')(*args)
def __rfloordiv__(self, *args):
return self._getSpecialAttr('__rfloordiv__')(*args)
def __rtruediv__(self, *args):
return self._getSpecialAttr('__rtruediv__')(*args)
def __rmul__(self, *args):
return self._getSpecialAttr('__rmul__')(*args)
@ -986,9 +1002,6 @@ class ObjectProxy(object):
def __rlshift__(self, *args):
return self._getSpecialAttr('__rlshift__')(*args)
def __rfloordiv__(self, *args):
return self._getSpecialAttr('__rpow__')(*args)
def __rand__(self, *args):
return self._getSpecialAttr('__rand__')(*args)
@ -1001,6 +1014,10 @@ class ObjectProxy(object):
def __rmod__(self, *args):
return self._getSpecialAttr('__rmod__')(*args)
def __hash__(self):
## Required for python3 since __eq__ is defined.
return id(self)
class DeferredObjectProxy(ObjectProxy):
"""
This class represents an attribute (or sub-attribute) of a proxied object.

View File

@ -40,6 +40,7 @@ class GLGraphicsItem(QtCore.QObject):
self.__glOpts = {}
def setParentItem(self, item):
"""Set this item's parent in the scenegraph hierarchy."""
if self.__parent is not None:
self.__parent.__children.remove(self)
if item is not None:
@ -98,9 +99,11 @@ class GLGraphicsItem(QtCore.QObject):
def parentItem(self):
"""Return a this item's parent in the scenegraph hierarchy."""
return self.__parent
def childItems(self):
"""Return a list of this item's children in the scenegraph hierarchy."""
return list(self.__children)
def _setView(self, v):
@ -116,27 +119,35 @@ class GLGraphicsItem(QtCore.QObject):
Items with negative depth values are drawn before their parent.
(This is analogous to QGraphicsItem.zValue)
The depthValue does NOT affect the position of the item or the values it imparts to the GL depth buffer.
'"""
"""
self.__depthValue = value
def depthValue(self):
"""Return the depth value of this item. See setDepthValue for mode information."""
"""Return the depth value of this item. See setDepthValue for more information."""
return self.__depthValue
def setTransform(self, tr):
"""Set the local transform for this object.
Must be a :class:`Transform3D <pyqtgraph.Transform3D>` instance. This transform
determines how the local coordinate system of the item is mapped to the coordinate
system of its parent."""
self.__transform = Transform3D(tr)
self.update()
def resetTransform(self):
"""Reset this item's transform to an identity transformation."""
self.__transform.setToIdentity()
self.update()
def applyTransform(self, tr, local):
"""
Multiply this object's transform by *tr*.
If local is True, then *tr* is multiplied on the right of the current transform:
If local is True, then *tr* is multiplied on the right of the current transform::
newTransform = transform * tr
If local is False, then *tr* is instead multiplied on the left:
If local is False, then *tr* is instead multiplied on the left::
newTransform = tr * transform
"""
if local:
@ -145,9 +156,12 @@ class GLGraphicsItem(QtCore.QObject):
self.setTransform(tr * self.transform())
def transform(self):
"""Return this item's transform object."""
return self.__transform
def viewTransform(self):
"""Return the transform mapping this item's local coordinate system to the
view coordinate system."""
tr = self.__transform
p = self
while True:
@ -187,16 +201,24 @@ class GLGraphicsItem(QtCore.QObject):
def hide(self):
"""Hide this item.
This is equivalent to setVisible(False)."""
self.setVisible(False)
def show(self):
"""Make this item visible if it was previously hidden.
This is equivalent to setVisible(True)."""
self.setVisible(True)
def setVisible(self, vis):
"""Set the visibility of this item."""
self.__visible = vis
self.update()
def visible(self):
"""Return True if the item is currently set to be visible.
Note that this does not guarantee that the item actually appears in the
view, as it may be obscured or outside of the current view area."""
return self.__visible
@ -234,10 +256,14 @@ class GLGraphicsItem(QtCore.QObject):
self.setupGLState()
def update(self):
"""
Indicates that this item needs to be redrawn, and schedules an update
with the view it is displayed in.
"""
v = self.view()
if v is None:
return
v.updateGL()
v.update()
def mapToParent(self, point):
tr = self.transform()

View File

@ -1,7 +1,10 @@
from pyqtgraph.Qt import QtCore, QtGui, QtOpenGL
from OpenGL.GL import *
import OpenGL.GL.framebufferobjects as glfbo
import numpy as np
from pyqtgraph import Vector
import pyqtgraph.functions as fn
##Vector = QtGui.QVector3D
class GLViewWidget(QtOpenGL.QGLWidget):
@ -31,6 +34,7 @@ class GLViewWidget(QtOpenGL.QGLWidget):
'elevation': 30, ## camera's angle of elevation in degrees
'azimuth': 45, ## camera's azimuthal angle in degrees
## (rotation around z-axis 0 points along x-axis)
'viewport': None, ## glViewport params; None == whole widget
}
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]
@ -63,55 +67,116 @@ class GLViewWidget(QtOpenGL.QGLWidget):
glClearColor(0.0, 0.0, 0.0, 0.0)
self.resizeGL(self.width(), self.height())
def getViewport(self):
vp = self.opts['viewport']
if vp is None:
return (0, 0, self.width(), self.height())
else:
return vp
def resizeGL(self, w, h):
glViewport(0, 0, w, h)
pass
#glViewport(*self.getViewport())
#self.update()
def setProjection(self):
## Create the projection matrix
def setProjection(self, region=None):
m = self.projectionMatrix(region)
glMatrixMode(GL_PROJECTION)
glLoadIdentity()
w = self.width()
h = self.height()
a = np.array(m.copyDataTo()).reshape((4,4))
glMultMatrixf(a.transpose())
def projectionMatrix(self, region=None):
# Xw = (Xnd + 1) * width/2 + X
if region is None:
region = (0, 0, self.width(), self.height())
x0, y0, w, h = self.getViewport()
dist = self.opts['distance']
fov = self.opts['fov']
nearClip = dist * 0.001
farClip = dist * 1000.
r = nearClip * np.tan(fov * 0.5 * np.pi / 180.)
t = r * h / w
glFrustum( -r, r, -t, t, nearClip, farClip)
# convert screen coordinates (region) to normalized device coordinates
# Xnd = (Xw - X0) * 2/width - 1
## Note that X0 and width in these equations must be the values used in viewport
left = r * ((region[0]-x0) * (2.0/w) - 1)
right = r * ((region[0]+region[2]-x0) * (2.0/w) - 1)
bottom = t * ((region[1]-y0) * (2.0/h) - 1)
top = t * ((region[1]+region[3]-y0) * (2.0/h) - 1)
tr = QtGui.QMatrix4x4()
tr.frustum(left, right, bottom, top, nearClip, farClip)
return tr
def setModelview(self):
glMatrixMode(GL_MODELVIEW)
glLoadIdentity()
glTranslatef( 0.0, 0.0, -self.opts['distance'])
glRotatef(self.opts['elevation']-90, 1, 0, 0)
glRotatef(self.opts['azimuth']+90, 0, 0, -1)
m = self.viewMatrix()
a = np.array(m.copyDataTo()).reshape((4,4))
glMultMatrixf(a.transpose())
def viewMatrix(self):
tr = QtGui.QMatrix4x4()
tr.translate( 0.0, 0.0, -self.opts['distance'])
tr.rotate(self.opts['elevation']-90, 1, 0, 0)
tr.rotate(self.opts['azimuth']+90, 0, 0, -1)
center = self.opts['center']
glTranslatef(-center.x(), -center.y(), -center.z())
tr.translate(-center.x(), -center.y(), -center.z())
return tr
def itemsAt(self, region=None):
#buf = np.zeros(100000, dtype=np.uint)
buf = glSelectBuffer(100000)
try:
glRenderMode(GL_SELECT)
glInitNames()
glPushName(0)
self._itemNames = {}
self.paintGL(region=region, useItemNames=True)
finally:
hits = glRenderMode(GL_RENDER)
items = [(h.near, h.names[0]) for h in hits]
items.sort(key=lambda i: i[0])
return [self._itemNames[i[1]] for i in items]
def paintGL(self):
self.setProjection()
def paintGL(self, region=None, viewport=None, useItemNames=False):
"""
viewport specifies the arguments to glViewport. If None, then we use self.opts['viewport']
region specifies the sub-region of self.opts['viewport'] that should be rendered.
Note that we may use viewport != self.opts['viewport'] when exporting.
"""
if viewport is None:
glViewport(*self.getViewport())
else:
glViewport(*viewport)
self.setProjection(region=region)
self.setModelview()
glClear( GL_DEPTH_BUFFER_BIT | GL_COLOR_BUFFER_BIT )
self.drawItemTree()
self.drawItemTree(useItemNames=useItemNames)
def drawItemTree(self, item=None):
def drawItemTree(self, item=None, useItemNames=False):
if item is None:
items = [x for x in self.items if x.parentItem() is None]
else:
items = item.childItems()
items.append(item)
items.sort(lambda a,b: cmp(a.depthValue(), b.depthValue()))
items.sort(key=lambda a: a.depthValue())
for i in items:
if not i.visible():
continue
if i is item:
try:
glPushAttrib(GL_ALL_ATTRIB_BITS)
if useItemNames:
glLoadName(id(i))
self._itemNames[id(i)] = i
i.paint()
except:
import pyqtgraph.debug
@ -120,7 +185,7 @@ class GLViewWidget(QtOpenGL.QGLWidget):
ver = glGetString(GL_VERSION)
if ver is not None:
ver = ver.split()[0]
if int(ver.split('.')[0]) < 2:
if int(ver.split(b'.')[0]) < 2:
print(msg + " The original exception is printed above; however, pyqtgraph requires OpenGL version 2.0 or greater for many of its 3D features and your OpenGL version is %s. Installing updated display drivers may resolve this issue." % ver)
else:
print(msg)
@ -134,7 +199,7 @@ class GLViewWidget(QtOpenGL.QGLWidget):
tr = i.transform()
a = np.array(tr.copyDataTo()).reshape((4,4))
glMultMatrixf(a.transpose())
self.drawItemTree(i)
self.drawItemTree(i, useItemNames=useItemNames)
finally:
glMatrixMode(GL_MODELVIEW)
glPopMatrix()
@ -168,6 +233,7 @@ class GLViewWidget(QtOpenGL.QGLWidget):
def orbit(self, azim, elev):
"""Orbits the camera around the center position. *azim* and *elev* are given in degrees."""
self.opts['azimuth'] += azim
#self.opts['elevation'] += elev
self.opts['elevation'] = np.clip(self.opts['elevation'] + elev, -90, 90)
self.update()
@ -286,4 +352,85 @@ class GLViewWidget(QtOpenGL.QGLWidget):
raise
def readQImage(self):
"""
Read the current buffer pixels out as a QImage.
"""
w = self.width()
h = self.height()
self.repaint()
pixels = np.empty((h, w, 4), dtype=np.ubyte)
pixels[:] = 128
pixels[...,0] = 50
pixels[...,3] = 255
glReadPixels(0, 0, w, h, GL_RGBA, GL_UNSIGNED_BYTE, pixels)
# swap B,R channels for Qt
tmp = pixels[...,0].copy()
pixels[...,0] = pixels[...,2]
pixels[...,2] = tmp
pixels = pixels[::-1] # flip vertical
img = fn.makeQImage(pixels, transpose=False)
return img
def renderToArray(self, size, format=GL_BGRA, type=GL_UNSIGNED_BYTE, textureSize=1024, padding=256):
w,h = map(int, size)
self.makeCurrent()
tex = None
fb = None
try:
output = np.empty((w, h, 4), dtype=np.ubyte)
fb = glfbo.glGenFramebuffers(1)
glfbo.glBindFramebuffer(glfbo.GL_FRAMEBUFFER, fb )
glEnable(GL_TEXTURE_2D)
tex = glGenTextures(1)
glBindTexture(GL_TEXTURE_2D, tex)
texwidth = textureSize
data = np.zeros((texwidth,texwidth,4), dtype=np.ubyte)
## Test texture dimensions first
glTexImage2D(GL_PROXY_TEXTURE_2D, 0, GL_RGBA, texwidth, texwidth, 0, GL_RGBA, GL_UNSIGNED_BYTE, None)
if glGetTexLevelParameteriv(GL_PROXY_TEXTURE_2D, 0, GL_TEXTURE_WIDTH) == 0:
raise Exception("OpenGL failed to create 2D texture (%dx%d); too large for this hardware." % shape[:2])
## create teture
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, texwidth, texwidth, 0, GL_RGBA, GL_UNSIGNED_BYTE, data.transpose((1,0,2)))
self.opts['viewport'] = (0, 0, w, h) # viewport is the complete image; this ensures that paintGL(region=...)
# is interpreted correctly.
p2 = 2 * padding
for x in range(-padding, w-padding, texwidth-p2):
for y in range(-padding, h-padding, texwidth-p2):
x2 = min(x+texwidth, w+padding)
y2 = min(y+texwidth, h+padding)
w2 = x2-x
h2 = y2-y
## render to texture
glfbo.glFramebufferTexture2D(glfbo.GL_FRAMEBUFFER, glfbo.GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, tex, 0)
self.paintGL(region=(x, h-y-h2, w2, h2), viewport=(0, 0, w2, h2)) # only render sub-region
## read texture back to array
data = glGetTexImage(GL_TEXTURE_2D, 0, format, type)
data = np.fromstring(data, dtype=np.ubyte).reshape(texwidth,texwidth,4).transpose(1,0,2)[:, ::-1]
output[x+padding:x2-padding, y+padding:y2-padding] = data[padding:w2-padding, -(h2-padding):-padding]
finally:
self.opts['viewport'] = None
glfbo.glBindFramebuffer(glfbo.GL_FRAMEBUFFER, 0)
glBindTexture(GL_TEXTURE_2D, 0)
if tex is not None:
glDeleteTextures([tex])
if fb is not None:
glfbo.glDeleteFramebuffers([fb])
return output

View File

@ -44,7 +44,7 @@ class MeshData(object):
## mappings between vertexes, faces, and edges
self._faces = None # Nx3 array of indexes into self._vertexes specifying three vertexes for each face
self._edges = None
self._edges = None # Nx2 array of indexes into self._vertexes specifying two vertexes per edge
self._vertexFaces = None ## maps vertex ID to a list of face IDs (inverse mapping of _faces)
self._vertexEdges = None ## maps vertex ID to a list of edge IDs (inverse mapping of _edges)
@ -143,12 +143,19 @@ class MeshData(object):
def faces(self):
"""Return an array (Nf, 3) of vertex indexes, three per triangular face in the mesh."""
return self._faces
def edges(self):
"""Return an array (Nf, 3) of vertex indexes, two per edge in the mesh."""
if self._edges is None:
self._computeEdges()
return self._edges
def setFaces(self, faces):
"""Set the (Nf, 3) array of faces. Each rown in the array contains
three indexes into the vertex array, specifying the three corners
of a triangular face."""
self._faces = faces
self._edges = None
self._vertexFaces = None
self._vertexesIndexedByFaces = None
self.resetNormals()
@ -240,9 +247,9 @@ class MeshData(object):
return self._faceNormals
elif indexed == 'faces':
if self._faceNormalsIndexedByFaces is None:
norms = np.empty((self._faceNormals.shape[0], 3, 3))
norms[:] = self._faceNormals[:,np.newaxis,:]
self._faceNormalsIndexedByFaces = norms
norms = np.empty((self._faceNormals.shape[0], 3, 3))
norms[:] = self._faceNormals[:,np.newaxis,:]
self._faceNormalsIndexedByFaces = norms
return self._faceNormalsIndexedByFaces
else:
raise Exception("Invalid indexing mode. Accepts: None, 'faces'")
@ -418,6 +425,25 @@ class MeshData(object):
#"""
#pass
def _computeEdges(self):
## 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]
edges['i'][nf:2*nf] = self._faces[:,1:3]
edges['i'][-nf:,0] = self._faces[:,2]
edges['i'][-nf:,1] = self._faces[:,0]
# sort per-edge
mask = edges['i'][:,0] > edges['i'][:,1]
edges['i'][mask] = edges['i'][mask][:,::-1]
# remove duplicate entries
self._edges = np.unique(edges)['i']
#print self._edges
def save(self):
"""Serialize this mesh to a string appropriate for disk storage"""
import pickle

View File

@ -23,8 +23,8 @@ from pyqtgraph import importAll
importAll('items', globals(), locals())
\
from MeshData import MeshData
from .MeshData import MeshData
## for backward compatibility:
#MeshData.MeshData = MeshData ## breaks autodoc.
import shaders
from . import shaders

View File

@ -12,11 +12,13 @@ class GLAxisItem(GLGraphicsItem):
"""
def __init__(self, size=None):
def __init__(self, size=None, antialias=True, glOptions='translucent'):
GLGraphicsItem.__init__(self)
if size is None:
size = QtGui.QVector3D(1,1,1)
self.antialias = antialias
self.setSize(size=size)
self.setGLOptions(glOptions)
def setSize(self, x=None, y=None, z=None, size=None):
"""
@ -36,11 +38,15 @@ class GLAxisItem(GLGraphicsItem):
def paint(self):
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA)
glEnable( GL_BLEND )
glEnable( GL_ALPHA_TEST )
glEnable( GL_POINT_SMOOTH )
#glDisable( GL_DEPTH_TEST )
#glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA)
#glEnable( GL_BLEND )
#glEnable( GL_ALPHA_TEST )
self.setupGLState()
if self.antialias:
glEnable(GL_LINE_SMOOTH)
glHint(GL_LINE_SMOOTH_HINT, GL_NICEST);
glBegin( GL_LINES )
x,y,z = self.size()

View File

@ -0,0 +1,29 @@
from .GLMeshItem import GLMeshItem
from ..MeshData import MeshData
import numpy as np
class GLBarGraphItem(GLMeshItem):
def __init__(self, pos, size):
"""
pos is (...,3) array of the bar positions (the corner of each bar)
size is (...,3) array of the sizes of each bar
"""
nCubes = reduce(lambda a,b: a*b, pos.shape[:-1])
cubeVerts = np.mgrid[0:2,0:2,0:2].reshape(3,8).transpose().reshape(1,8,3)
cubeFaces = np.array([
[0,1,2], [3,2,1],
[4,5,6], [7,6,5],
[0,1,4], [5,4,1],
[2,3,6], [7,6,3],
[0,2,4], [6,4,2],
[1,3,5], [7,5,3]]).reshape(1,12,3)
size = size.reshape((nCubes, 1, 3))
pos = pos.reshape((nCubes, 1, 3))
verts = cubeVerts * size + pos
faces = cubeFaces + (np.arange(nCubes) * 8).reshape(nCubes,1,1)
md = MeshData(verts.reshape(nCubes*8,3), faces.reshape(nCubes*12,3))
GLMeshItem.__init__(self, meshdata=md, shader='shaded', smooth=False)

View File

@ -11,7 +11,7 @@ class GLBoxItem(GLGraphicsItem):
Displays a wire-frame box.
"""
def __init__(self, size=None, color=None):
def __init__(self, size=None, color=None, glOptions='translucent'):
GLGraphicsItem.__init__(self)
if size is None:
size = QtGui.QVector3D(1,1,1)
@ -19,6 +19,7 @@ class GLBoxItem(GLGraphicsItem):
if color is None:
color = (255,255,255,80)
self.setColor(color)
self.setGLOptions(glOptions)
def setSize(self, x=None, y=None, z=None, size=None):
"""
@ -43,12 +44,14 @@ class GLBoxItem(GLGraphicsItem):
return self.__color
def paint(self):
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA)
glEnable( GL_BLEND )
glEnable( GL_ALPHA_TEST )
#glAlphaFunc( GL_ALWAYS,0.5 )
glEnable( GL_POINT_SMOOTH )
glDisable( GL_DEPTH_TEST )
#glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA)
#glEnable( GL_BLEND )
#glEnable( GL_ALPHA_TEST )
##glAlphaFunc( GL_ALWAYS,0.5 )
#glEnable( GL_POINT_SMOOTH )
#glDisable( GL_DEPTH_TEST )
self.setupGLState()
glBegin( GL_LINES )
glColor4f(*self.color().glColor())

View File

@ -11,9 +11,10 @@ class GLGridItem(GLGraphicsItem):
Displays a wire-grame grid.
"""
def __init__(self, size=None, color=None, glOptions='translucent'):
def __init__(self, size=None, color=None, antialias=True, glOptions='translucent'):
GLGraphicsItem.__init__(self)
self.setGLOptions(glOptions)
self.antialias = antialias
if size is None:
size = QtGui.QVector3D(1,1,1)
self.setSize(size=size)
@ -36,11 +37,13 @@ class GLGridItem(GLGraphicsItem):
def paint(self):
self.setupGLState()
#glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA)
#glEnable( GL_BLEND )
#glEnable( GL_ALPHA_TEST )
glEnable( GL_POINT_SMOOTH )
#glDisable( GL_DEPTH_TEST )
if self.antialias:
glEnable(GL_LINE_SMOOTH)
glEnable(GL_BLEND)
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA)
glHint(GL_LINE_SMOOTH_HINT, GL_NICEST);
glBegin( GL_LINES )
x,y,z = self.size()

View File

@ -13,7 +13,7 @@ class GLImageItem(GLGraphicsItem):
"""
def __init__(self, data, smooth=False):
def __init__(self, data, smooth=False, glOptions='translucent'):
"""
============== =======================================================================================
@ -27,6 +27,7 @@ class GLImageItem(GLGraphicsItem):
self.smooth = smooth
self.data = data
GLGraphicsItem.__init__(self)
self.setGLOptions(glOptions)
def initializeGL(self):
glEnable(GL_TEXTURE_2D)
@ -66,11 +67,13 @@ class GLImageItem(GLGraphicsItem):
glEnable(GL_TEXTURE_2D)
glBindTexture(GL_TEXTURE_2D, self.texture)
glEnable(GL_DEPTH_TEST)
#glDisable(GL_CULL_FACE)
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA)
glEnable( GL_BLEND )
glEnable( GL_ALPHA_TEST )
self.setupGLState()
#glEnable(GL_DEPTH_TEST)
##glDisable(GL_CULL_FACE)
#glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA)
#glEnable( GL_BLEND )
#glEnable( GL_ALPHA_TEST )
glColor4f(1,1,1,1)
glBegin(GL_QUADS)

View File

@ -11,6 +11,7 @@ class GLLinePlotItem(GLGraphicsItem):
"""Draws line plots in 3D."""
def __init__(self, **kwds):
"""All keyword arguments are passed to setData()"""
GLGraphicsItem.__init__(self)
glopts = kwds.pop('glOptions', 'additive')
self.setGLOptions(glopts)
@ -22,23 +23,25 @@ class GLLinePlotItem(GLGraphicsItem):
def setData(self, **kwds):
"""
Update the data displayed by this item. All arguments are optional;
for example it is allowed to update spot positions while leaving
for example it is allowed to update vertex positions while leaving
colors unchanged, etc.
==================== ==================================================
Arguments:
------------------------------------------------------------------------
pos (N,3) array of floats specifying point locations.
color tuple of floats (0.0-1.0) specifying
a color for the entire item.
color (N,4) array of floats (0.0-1.0) or
tuple of floats specifying
a single color for the entire item.
width float specifying line width
antialias enables smooth line drawing
==================== ==================================================
"""
args = ['pos', 'color', 'width', 'connected']
args = ['pos', 'color', 'width', 'connected', 'antialias']
for k in kwds.keys():
if k not in args:
raise Exception('Invalid keyword argument: %s (allowed arguments are %s)' % (k, str(args)))
self.antialias = False
for arg in args:
if arg in kwds:
setattr(self, arg, kwds[arg])
@ -69,13 +72,30 @@ class GLLinePlotItem(GLGraphicsItem):
self.setupGLState()
glEnableClientState(GL_VERTEX_ARRAY)
try:
glVertexPointerf(self.pos)
glColor4f(*self.color)
glPointSize(self.width)
glDrawArrays(GL_LINE_STRIP, 0, self.pos.size / self.pos.shape[-1])
if isinstance(self.color, np.ndarray):
glEnableClientState(GL_COLOR_ARRAY)
glColorPointerf(self.color)
else:
if isinstance(self.color, QtGui.QColor):
glColor4f(*fn.glColor(self.color))
else:
glColor4f(*self.color)
glLineWidth(self.width)
#glPointSize(self.width)
if self.antialias:
glEnable(GL_LINE_SMOOTH)
glEnable(GL_BLEND)
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA)
glHint(GL_LINE_SMOOTH_HINT, GL_NICEST);
glDrawArrays(GL_LINE_STRIP, 0, int(self.pos.size / self.pos.shape[-1]))
finally:
glDisableClientState(GL_COLOR_ARRAY)
glDisableClientState(GL_VERTEX_ARRAY)

View File

@ -22,9 +22,15 @@ class GLMeshItem(GLGraphicsItem):
Arguments
meshdata MeshData object from which to determine geometry for
this item.
color Default color used if no vertex or face colors are
specified.
shader Name of shader program to use (None for no shader)
color Default face color used if no vertex or face colors
are specified.
edgeColor Default edge color to use if no edge colors are
specified in the mesh data.
drawEdges If True, a wireframe mesh will be drawn.
(default=False)
drawFaces If True, mesh faces are drawn. (default=True)
shader Name of shader program to use when drawing faces.
(None for no shader)
smooth If True, normal vectors are computed for each vertex
and interpolated within each face.
computeNormals If False, then computation of normal vectors is
@ -35,6 +41,9 @@ class GLMeshItem(GLGraphicsItem):
self.opts = {
'meshdata': None,
'color': (1., 1., 1., 1.),
'drawEdges': False,
'drawFaces': True,
'edgeColor': (0.5, 0.5, 0.5, 1.0),
'shader': None,
'smooth': True,
'computeNormals': True,
@ -60,7 +69,11 @@ class GLMeshItem(GLGraphicsItem):
self.update()
def shader(self):
return shaders.getShaderProgram(self.opts['shader'])
shader = self.opts['shader']
if isinstance(shader, shaders.ShaderProgram):
return shader
else:
return shaders.getShaderProgram(shader)
def setColor(self, c):
"""Set the default color to use when no vertex or face colors are specified."""
@ -100,6 +113,8 @@ class GLMeshItem(GLGraphicsItem):
self.faces = None
self.normals = None
self.colors = None
self.edges = None
self.edgeColors = None
self.update()
def parseMeshData(self):
@ -137,6 +152,9 @@ class GLMeshItem(GLGraphicsItem):
elif md.hasFaceColor():
self.colors = md.faceColors(indexed='faces')
if self.opts['drawEdges']:
self.edges = md.edges()
self.edgeVerts = md.vertexes()
return
def paint(self):
@ -144,19 +162,52 @@ class GLMeshItem(GLGraphicsItem):
self.parseMeshData()
with self.shader():
verts = self.vertexes
norms = self.normals
color = self.colors
faces = self.faces
if verts is None:
return
if self.opts['drawFaces']:
with self.shader():
verts = self.vertexes
norms = self.normals
color = self.colors
faces = self.faces
if verts is None:
return
glEnableClientState(GL_VERTEX_ARRAY)
try:
glVertexPointerf(verts)
if self.colors is None:
color = self.opts['color']
if isinstance(color, QtGui.QColor):
glColor4f(*pg.glColor(color))
else:
glColor4f(*color)
else:
glEnableClientState(GL_COLOR_ARRAY)
glColorPointerf(color)
if norms is not None:
glEnableClientState(GL_NORMAL_ARRAY)
glNormalPointerf(norms)
if faces is None:
glDrawArrays(GL_TRIANGLES, 0, np.product(verts.shape[:-1]))
else:
faces = faces.astype(np.uint).flatten()
glDrawElements(GL_TRIANGLES, faces.shape[0], GL_UNSIGNED_INT, faces)
finally:
glDisableClientState(GL_NORMAL_ARRAY)
glDisableClientState(GL_VERTEX_ARRAY)
glDisableClientState(GL_COLOR_ARRAY)
if self.opts['drawEdges']:
verts = self.edgeVerts
edges = self.edges
glEnableClientState(GL_VERTEX_ARRAY)
try:
glVertexPointerf(verts)
if self.colors is None:
color = self.opts['color']
if self.edgeColors is None:
color = self.opts['edgeColor']
if isinstance(color, QtGui.QColor):
glColor4f(*pg.glColor(color))
else:
@ -164,19 +215,9 @@ class GLMeshItem(GLGraphicsItem):
else:
glEnableClientState(GL_COLOR_ARRAY)
glColorPointerf(color)
if norms is not None:
glEnableClientState(GL_NORMAL_ARRAY)
glNormalPointerf(norms)
if faces is None:
glDrawArrays(GL_TRIANGLES, 0, np.product(verts.shape[:-1]))
else:
faces = faces.astype(np.uint).flatten()
glDrawElements(GL_TRIANGLES, faces.shape[0], GL_UNSIGNED_INT, faces)
edges = edges.flatten()
glDrawElements(GL_LINES, edges.shape[0], GL_UNSIGNED_INT, edges)
finally:
glDisableClientState(GL_NORMAL_ARRAY)
glDisableClientState(GL_VERTEX_ARRAY)
glDisableClientState(GL_COLOR_ARRAY)

View File

@ -146,7 +146,7 @@ class GLScatterPlotItem(GLGraphicsItem):
else:
glNormal3f(self.size, 0, 0) ## vertex shader uses norm.x to determine point size
#glPointSize(self.size)
glDrawArrays(GL_POINTS, 0, pos.size / pos.shape[-1])
glDrawArrays(GL_POINTS, 0, int(pos.size / pos.shape[-1]))
finally:
glDisableClientState(GL_NORMAL_ARRAY)
glDisableClientState(GL_VERTEX_ARRAY)

View File

@ -1,5 +1,5 @@
from OpenGL.GL import *
from GLMeshItem import GLMeshItem
from .GLMeshItem import GLMeshItem
from .. MeshData import MeshData
from pyqtgraph.Qt import QtGui
import pyqtgraph as pg
@ -136,4 +136,4 @@ class GLSurfacePlotItem(GLMeshItem):
start = row * cols * 2
faces[start:start+cols] = rowtemplate1 + row * (cols+1)
faces[start+cols:start+(cols*2)] = rowtemplate2 + row * (cols+1)
self._faces = faces
self._faces = faces

View File

@ -1,3 +1,7 @@
try:
from OpenGL import NullFunctionError
except ImportError:
from OpenGL.error import NullFunctionError
from OpenGL.GL import *
from OpenGL.GL import shaders
import re
@ -218,15 +222,20 @@ class Shader(object):
if self.compiled is None:
try:
self.compiled = shaders.compileShader(self.code, self.shaderType)
except NullFunctionError:
raise Exception("This OpenGL implementation does not support shader programs; many OpenGL features in pyqtgraph will not work.")
except RuntimeError as exc:
## Format compile errors a bit more nicely
if len(exc.args) == 3:
err, code, typ = exc.args
if not err.startswith('Shader compile failure'):
raise
code = code[0].split('\n')
code = code[0].decode('utf_8').split('\n')
err, c, msgs = err.partition(':')
err = err + '\n'
msgs = re.sub('b\'','',msgs)
msgs = re.sub('\'$','',msgs)
msgs = re.sub('\\\\n','\n',msgs)
msgs = msgs.split('\n')
errNums = [()] * len(code)
for i, msg in enumerate(msgs):
@ -354,7 +363,7 @@ class ShaderProgram(object):
def uniform(self, name):
"""Return the location integer for a uniform variable in this program"""
return glGetUniformLocation(self.program(), name)
return glGetUniformLocation(self.program(), name.encode('utf_8'))
#def uniformBlockInfo(self, blockName):
#blockIndex = glGetUniformBlockIndex(self.program(), blockName)
@ -390,4 +399,4 @@ class HeightColorShader(ShaderProgram):
## bind buffer to the same binding point
glBindBufferBase(GL_UNIFORM_BUFFER, bindPoint, buf)
initShaders()
initShaders()

127
ordereddict.py Normal file
View File

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

View File

@ -157,3 +157,9 @@ class ParameterItem(QtGui.QTreeWidgetItem):
## since destroying the menu in mid-action will cause a crash.
QtCore.QTimer.singleShot(0, self.param.remove)
## for python 3 support, we need to redefine hash and eq methods.
def __hash__(self):
return id(self)
def __eq__(self, x):
return x is self

View File

@ -1,6 +1,7 @@
from pyqtgraph.Qt import QtCore, QtGui
from pyqtgraph.widgets.TreeWidget import TreeWidget
import os, weakref, re
from .ParameterItem import ParameterItem
#import functions as fn
@ -103,7 +104,7 @@ class ParameterTree(TreeWidget):
sel = self.selectedItems()
if len(sel) != 1:
sel = None
if self.lastSel is not None:
if self.lastSel is not None and isinstance(self.lastSel, ParameterItem):
self.lastSel.selected(False)
if sel is None:
self.lastSel = None

View File

@ -476,32 +476,16 @@ class ListParameterItem(WidgetParameterItem):
return w
def value(self):
#vals = self.param.opts['limits']
key = asUnicode(self.widget.currentText())
#if isinstance(vals, dict):
#return vals[key]
#else:
#return key
#print key, self.forward
return self.forward.get(key, None)
def setValue(self, val):
#vals = self.param.opts['limits']
#if isinstance(vals, dict):
#key = None
#for k,v in vals.iteritems():
#if v == val:
#key = k
#if key is None:
#raise Exception("Value '%s' not allowed." % val)
#else:
#key = unicode(val)
self.targetValue = val
if val not in self.reverse:
if val not in self.reverse[0]:
self.widget.setCurrentIndex(0)
else:
key = self.reverse[val]
key = self.reverse[1][self.reverse[0].index(val)]
ind = self.widget.findText(key)
self.widget.setCurrentIndex(ind)
@ -531,8 +515,8 @@ class ListParameter(Parameter):
itemClass = ListParameterItem
def __init__(self, **opts):
self.forward = OrderedDict() ## name: value
self.reverse = OrderedDict() ## value: name
self.forward = OrderedDict() ## {name: value, ...}
self.reverse = ([], []) ## ([value, ...], [name, ...])
## Parameter uses 'limits' option to define the set of allowed values
if 'values' in opts:
@ -547,23 +531,40 @@ class ListParameter(Parameter):
Parameter.setLimits(self, limits)
#print self.name(), self.value(), limits
if self.value() not in self.reverse and len(self.reverse) > 0:
self.setValue(list(self.reverse.keys())[0])
if len(self.reverse) > 0 and self.value() not in self.reverse[0]:
self.setValue(self.reverse[0][0])
#def addItem(self, name, value=None):
#if name in self.forward:
#raise Exception("Name '%s' is already in use for this parameter" % name)
#limits = self.opts['limits']
#if isinstance(limits, dict):
#limits = limits.copy()
#limits[name] = value
#self.setLimits(limits)
#else:
#if value is not None:
#raise Exception ## raise exception or convert to dict?
#limits = limits[:]
#limits.append(name)
## what if limits == None?
@staticmethod
def mapping(limits):
## Return forward and reverse mapping dictionaries given a limit specification
forward = OrderedDict() ## name: value
reverse = OrderedDict() ## value: name
## Return forward and reverse mapping objects given a limit specification
forward = OrderedDict() ## {name: value, ...}
reverse = ([], []) ## ([value, ...], [name, ...])
if isinstance(limits, dict):
for k, v in limits.items():
forward[k] = v
reverse[v] = k
reverse[0].append(v)
reverse[1].append(k)
else:
for v in limits:
n = asUnicode(v)
forward[n] = v
reverse[v] = n
reverse[0].append(v)
reverse[1].append(n)
return forward, reverse
registerParameterType('list', ListParameter, override=True)
@ -615,13 +616,20 @@ registerParameterType('action', ActionParameter, override=True)
class TextParameterItem(WidgetParameterItem):
def __init__(self, param, depth):
WidgetParameterItem.__init__(self, param, depth)
self.hideWidget = False
self.subItem = QtGui.QTreeWidgetItem()
self.addChild(self.subItem)
def treeWidgetChanged(self):
## TODO: fix so that superclass method can be called
## (WidgetParameter should just natively support this style)
#WidgetParameterItem.treeWidgetChanged(self)
self.treeWidget().setFirstItemColumnSpanned(self.subItem, True)
self.treeWidget().setItemWidget(self.subItem, 0, self.textBox)
self.setExpanded(True)
# for now, these are copied from ParameterItem.treeWidgetChanged
self.setHidden(not self.param.opts.get('visible', True))
self.setExpanded(self.param.opts.get('expanded', True))
def makeWidget(self):
self.textBox = QtGui.QTextEdit()

View File

@ -15,75 +15,9 @@ import threading, sys, copy, collections
try:
from collections import OrderedDict
except:
# Deprecated; this class is now present in Python 2.7 as collections.OrderedDict
# Only keeping this around for python2.6 support.
class OrderedDict(dict):
"""extends dict so that elements are iterated in the order that they were added.
Since this class can not be instantiated with regular dict notation, it instead uses
a list of tuples:
od = OrderedDict([(key1, value1), (key2, value2), ...])
items set using __setattr__ are added to the end of the key list.
"""
def __init__(self, data=None):
self.order = []
if data is not None:
for i in data:
self[i[0]] = i[1]
def __setitem__(self, k, v):
if not self.has_key(k):
self.order.append(k)
dict.__setitem__(self, k, v)
def __delitem__(self, k):
self.order.remove(k)
dict.__delitem__(self, k)
def keys(self):
return self.order[:]
def items(self):
it = []
for k in self.keys():
it.append((k, self[k]))
return it
def values(self):
return [self[k] for k in self.order]
def remove(self, key):
del self[key]
#self.order.remove(key)
def __iter__(self):
for k in self.order:
yield k
def update(self, data):
"""Works like dict.update, but accepts list-of-tuples as well as dict."""
if isinstance(data, dict):
for k, v in data.iteritems():
self[k] = v
else:
for k,v in data:
self[k] = v
def copy(self):
return OrderedDict(self.items())
def itervalues(self):
for k in self.order:
yield self[k]
def iteritems(self):
for k in self.order:
yield (k, self[k])
def __deepcopy__(self, memo):
return OrderedDict([(k, copy.deepcopy(v, memo)) for k, v in self.iteritems()])
except ImportError:
# fallback: try to use the ordereddict backport when using python 2.6
from ordereddict import OrderedDict
class ReverseDict(dict):
@ -540,4 +474,4 @@ if __name__ == '__main__':
lp = protect(l)
t = (1, 'x', ['a', 'b'], ('c', 'd'), {'x': 1, 'y': 2})
tp = protect(t)
tp = protect(t)

View File

@ -72,7 +72,8 @@ class ColorMapParameter(ptree.types.GroupParameter):
(see *values* option).
units String indicating the units of the data for this field.
values List of unique values for which the user may assign a
color when mode=='enum'.
color when mode=='enum'. Optionally may specify a dict
instead {value: name}.
============== ============================================================
"""
self.fields = OrderedDict(fields)
@ -168,7 +169,16 @@ class EnumColorMapItem(ptree.types.GroupParameter):
def __init__(self, name, opts):
self.fieldName = name
vals = opts.get('values', [])
if isinstance(vals, list):
vals = OrderedDict([(v,str(v)) for v in vals])
childs = [{'name': v, 'type': 'color'} for v in vals]
childs = []
for val,vname in vals.items():
ch = ptree.Parameter.create(name=vname, type='color')
ch.maskValue = val
childs.append(ch)
ptree.types.GroupParameter.__init__(self,
name=name, autoIncrementName=True, removable=True, renamable=True,
children=[
@ -191,8 +201,7 @@ class EnumColorMapItem(ptree.types.GroupParameter):
colors[:] = default
for v in self.param('Values'):
n = v.name()
mask = data == n
mask = data == v.maskValue
c = np.array(fn.colorTuple(v.value())) / 255.
colors[mask] = c
#scaled = np.clip((data-self['Min']) / (self['Max']-self['Min']), 0, 1)

View File

@ -2,6 +2,7 @@ from pyqtgraph.Qt import QtGui, QtCore
import pyqtgraph.parametertree as ptree
import numpy as np
from pyqtgraph.pgcollections import OrderedDict
import pyqtgraph as pg
__all__ = ['DataFilterWidget']
@ -22,6 +23,7 @@ class DataFilterWidget(ptree.ParameterTree):
self.setFields = self.params.setFields
self.filterData = self.params.filterData
self.describe = self.params.describe
def filterChanged(self):
self.sigFilterChanged.emit(self)
@ -70,18 +72,28 @@ class DataFilterParameter(ptree.types.GroupParameter):
for fp in self:
if fp.value() is False:
continue
mask &= fp.generateMask(data)
mask &= fp.generateMask(data, mask.copy())
#key, mn, mx = fp.fieldName, fp['Min'], fp['Max']
#vals = data[key]
#mask &= (vals >= mn)
#mask &= (vals < mx) ## Use inclusive minimum and non-inclusive maximum. This makes it easier to create non-overlapping selections
return mask
def describe(self):
"""Return a list of strings describing the currently enabled filters."""
desc = []
for fp in self:
if fp.value() is False:
continue
desc.append(fp.describe())
return desc
class RangeFilterItem(ptree.types.SimpleParameter):
def __init__(self, name, opts):
self.fieldName = name
units = opts.get('units', '')
self.units = units
ptree.types.SimpleParameter.__init__(self,
name=name, autoIncrementName=True, type='bool', value=True, removable=True, renamable=True,
children=[
@ -90,26 +102,49 @@ class RangeFilterItem(ptree.types.SimpleParameter):
dict(name='Max', type='float', value=1.0, suffix=units, siPrefix=True),
])
def generateMask(self, data):
vals = data[self.fieldName]
return (vals >= mn) & (vals < mx) ## Use inclusive minimum and non-inclusive maximum. This makes it easier to create non-overlapping selections
def generateMask(self, data, mask):
vals = data[self.fieldName][mask]
mask[mask] = (vals >= self['Min']) & (vals < self['Max']) ## Use inclusive minimum and non-inclusive maximum. This makes it easier to create non-overlapping selections
return mask
def describe(self):
return "%s < %s < %s" % (pg.siFormat(self['Min'], suffix=self.units), self.fieldName, pg.siFormat(self['Max'], suffix=self.units))
class EnumFilterItem(ptree.types.SimpleParameter):
def __init__(self, name, opts):
self.fieldName = name
vals = opts.get('values', [])
childs = [{'name': v, 'type': 'bool', 'value': True} for v in vals]
childs = []
if isinstance(vals, list):
vals = OrderedDict([(v,str(v)) for v in vals])
for val,vname in vals.items():
ch = ptree.Parameter.create(name=vname, type='bool', value=True)
ch.maskValue = val
childs.append(ch)
ch = ptree.Parameter.create(name='(other)', type='bool', value=True)
ch.maskValue = '__other__'
childs.append(ch)
ptree.types.SimpleParameter.__init__(self,
name=name, autoIncrementName=True, type='bool', value=True, removable=True, renamable=True,
children=childs)
def generateMask(self, data):
vals = data[self.fieldName]
mask = np.ones(len(data), dtype=bool)
def generateMask(self, data, startMask):
vals = data[self.fieldName][startMask]
mask = np.ones(len(vals), dtype=bool)
otherMask = np.ones(len(vals), dtype=bool)
for c in self:
if c.value() is True:
continue
key = c.name()
mask &= vals != key
return mask
key = c.maskValue
if key == '__other__':
m = ~otherMask
else:
m = vals != key
otherMask &= m
if c.value() is False:
mask &= m
startMask[startMask] = mask
return startMask
def describe(self):
vals = [ch.name() for ch in self if ch.value() is True]
return "%s: %s" % (self.fieldName, ', '.join(vals))

View File

@ -82,6 +82,7 @@ class GraphicsView(QtGui.QGraphicsView):
## This might help, but it's probably dangerous in the general case..
#self.setOptimizationFlag(self.DontSavePainterState, True)
self.setBackgroundRole(QtGui.QPalette.NoRole)
self.setBackground(background)
self.setFocusPolicy(QtCore.Qt.StrongFocus)
@ -138,17 +139,19 @@ class GraphicsView(QtGui.QGraphicsView):
self._background = background
if background == 'default':
background = pyqtgraph.getConfigOption('background')
if background is None:
self.setBackgroundRole(QtGui.QPalette.NoRole)
else:
brush = fn.mkBrush(background)
self.setBackgroundBrush(brush)
brush = fn.mkBrush(background)
self.setBackgroundBrush(brush)
def paintEvent(self, ev):
self.scene().prepareForPaint()
#print "GV: paint", ev.rect()
return QtGui.QGraphicsView.paintEvent(self, ev)
def render(self, *args, **kwds):
self.scene().prepareForPaint()
return QtGui.QGraphicsView.render(self, *args, **kwds)
def close(self):
self.centralWidget = None
self.scene().clear()
@ -181,8 +184,9 @@ class GraphicsView(QtGui.QGraphicsView):
if self.centralWidget is not None:
self.scene().removeItem(self.centralWidget)
self.centralWidget = item
self.sceneObj.addItem(item)
self.resizeEvent(None)
if item is not None:
self.sceneObj.addItem(item)
self.resizeEvent(None)
def addItem(self, *args):
return self.scene().addItem(*args)
@ -272,7 +276,8 @@ class GraphicsView(QtGui.QGraphicsView):
scaleChanged = True
self.range = newRect
#print "New Range:", self.range
self.centralWidget.setGeometry(self.range)
if self.centralWidget is not None:
self.centralWidget.setGeometry(self.range)
self.updateMatrix(propagate)
if scaleChanged:
self.sigScaleChanged.emit(self)

View File

@ -13,7 +13,7 @@ __all__ = ['HistogramLUTWidget']
class HistogramLUTWidget(GraphicsView):
def __init__(self, parent=None, *args, **kargs):
background = kargs.get('background', 'k')
background = kargs.get('background', 'default')
GraphicsView.__init__(self, parent, useOpenGL=False, background=background)
self.item = HistogramLUTItem(*args, **kargs)
self.setCentralItem(self.item)

View File

@ -1,5 +1,9 @@
from pyqtgraph.Qt import QtGui, QtCore
from pyqtgraph.Qt import QtGui, QtCore, USE_PYSIDE
import matplotlib
if USE_PYSIDE:
matplotlib.rcParams['backend.qt4']='PySide'
from matplotlib.backends.backend_qt4agg import FigureCanvasQTAgg as FigureCanvas
from matplotlib.backends.backend_qt4agg import NavigationToolbar2QTAgg as NavigationToolbar
from matplotlib.figure import Figure

View File

@ -40,10 +40,12 @@ class PlotWidget(GraphicsView):
For all
other methods, use :func:`getPlotItem <pyqtgraph.PlotWidget.getPlotItem>`.
"""
def __init__(self, parent=None, **kargs):
"""When initializing PlotWidget, all keyword arguments except *parent* are passed
def __init__(self, parent=None, background='default', **kargs):
"""When initializing PlotWidget, *parent* and *background* are passed to
:func:`GraphicsWidget.__init__() <pyqtgraph.GraphicsWidget.__init__>`
and all others are passed
to :func:`PlotItem.__init__() <pyqtgraph.PlotItem.__init__>`."""
GraphicsView.__init__(self, parent)
GraphicsView.__init__(self, parent, background=background)
self.setSizePolicy(QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Expanding)
self.enableMouse(False)
self.plotItem = PlotItem(**kargs)

View File

@ -1,6 +1,7 @@
from pyqtgraph.Qt import QtCore, QtGui
try:
from pyqtgraph.Qt import QtOpenGL
from OpenGL.GL import *
HAVE_OPENGL = True
except ImportError:
HAVE_OPENGL = False
@ -11,8 +12,8 @@ import numpy as np
class RawImageWidget(QtGui.QWidget):
"""
Widget optimized for very fast video display.
Generally using an ImageItem inside GraphicsView is fast enough,
but if you need even more performance, this widget is about as fast as it gets (but only in unscaled mode).
Generally using an ImageItem inside GraphicsView is fast enough.
On some systems this may provide faster video. See the VideoSpeedTest example for benchmarking.
"""
def __init__(self, parent=None, scaled=False):
"""
@ -62,23 +63,78 @@ if HAVE_OPENGL:
class RawImageGLWidget(QtOpenGL.QGLWidget):
"""
Similar to RawImageWidget, but uses a GL widget to do all drawing.
Generally this will be about as fast as using GraphicsView + ImageItem,
but performance may vary on some platforms.
Perfomance varies between platforms; see examples/VideoSpeedTest for benchmarking.
"""
def __init__(self, parent=None, scaled=False):
QtOpenGL.QGLWidget.__init__(self, parent=None)
self.scaled = scaled
self.image = None
self.uploaded = False
self.smooth = False
self.opts = None
def setImage(self, img):
self.image = fn.makeQImage(img)
def setImage(self, img, *args, **kargs):
"""
img must be ndarray of shape (x,y), (x,y,3), or (x,y,4).
Extra arguments are sent to functions.makeARGB
"""
self.opts = (img, args, kargs)
self.image = None
self.uploaded = False
self.update()
def paintEvent(self, ev):
def initializeGL(self):
self.texture = glGenTextures(1)
def uploadTexture(self):
glEnable(GL_TEXTURE_2D)
glBindTexture(GL_TEXTURE_2D, self.texture)
if self.smooth:
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR)
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR)
else:
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST)
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST)
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_BORDER)
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_BORDER)
#glTexParameteri(GL_TEXTURE_3D, GL_TEXTURE_WRAP_R, GL_CLAMP_TO_BORDER)
shape = self.image.shape
### Test texture dimensions first
#glTexImage2D(GL_PROXY_TEXTURE_2D, 0, GL_RGBA, shape[0], shape[1], 0, GL_RGBA, GL_UNSIGNED_BYTE, None)
#if glGetTexLevelParameteriv(GL_PROXY_TEXTURE_2D, 0, GL_TEXTURE_WIDTH) == 0:
#raise Exception("OpenGL failed to create 2D texture (%dx%d); too large for this hardware." % shape[:2])
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, shape[0], shape[1], 0, GL_RGBA, GL_UNSIGNED_BYTE, self.image.transpose((1,0,2)))
glDisable(GL_TEXTURE_2D)
def paintGL(self):
if self.image is None:
return
p = QtGui.QPainter(self)
p.drawImage(self.rect(), self.image)
p.end()
if self.opts is None:
return
img, args, kwds = self.opts
kwds['useRGBA'] = True
self.image, alpha = fn.makeARGB(img, *args, **kwds)
if not self.uploaded:
self.uploadTexture()
glViewport(0, 0, self.width(), self.height())
glEnable(GL_TEXTURE_2D)
glBindTexture(GL_TEXTURE_2D, self.texture)
glColor4f(1,1,1,1)
glBegin(GL_QUADS)
glTexCoord2f(0,0)
glVertex3f(-1,-1,0)
glTexCoord2f(1,0)
glVertex3f(1, -1, 0)
glTexCoord2f(1,1)
glVertex3f(1, 1, 0)
glTexCoord2f(0,1)
glVertex3f(-1, 1, 0)
glEnd()
glDisable(GL_TEXTURE_3D)

View File

@ -1,4 +1,6 @@
from pyqtgraph.Qt import QtGui, QtCore, USE_PYSIDE
if not USE_PYSIDE:
import sip
import pyqtgraph.multiprocess as mp
import pyqtgraph as pg
from .GraphicsView import GraphicsView
@ -16,16 +18,27 @@ class RemoteGraphicsView(QtGui.QWidget):
"""
def __init__(self, parent=None, *args, **kwds):
"""
The keyword arguments 'useOpenGL' and 'backgound', if specified, are passed to the remote
GraphicsView.__init__(). All other keyword arguments are passed to multiprocess.QtProcess.__init__().
"""
self._img = None
self._imgReq = None
self._sizeHint = (640,480) ## no clue why this is needed, but it seems to be the default sizeHint for GraphicsView.
## without it, the widget will not compete for space against another GraphicsView.
QtGui.QWidget.__init__(self)
self._proc = mp.QtProcess(debug=False)
# separate local keyword arguments from remote.
remoteKwds = {}
for kwd in ['useOpenGL', 'background']:
if kwd in kwds:
remoteKwds[kwd] = kwds.pop(kwd)
self._proc = mp.QtProcess(**kwds)
self.pg = self._proc._import('pyqtgraph')
self.pg.setConfigOptions(**self.pg.CONFIG_OPTIONS)
rpgRemote = self._proc._import('pyqtgraph.widgets.RemoteGraphicsView')
self._view = rpgRemote.Renderer(*args, **kwds)
self._view = rpgRemote.Renderer(*args, **remoteKwds)
self._view._setProxyOptions(deferGetattr=True)
self.setFocusPolicy(QtCore.Qt.StrongFocus)
@ -67,7 +80,9 @@ class RemoteGraphicsView(QtGui.QWidget):
else:
self.shm = mmap.mmap(self.shmFile.fileno(), size, mmap.MAP_SHARED, mmap.PROT_READ)
self.shm.seek(0)
self._img = QtGui.QImage(self.shm.read(w*h*4), w, h, QtGui.QImage.Format_ARGB32)
data = self.shm.read(w*h*4)
self._img = QtGui.QImage(data, w, h, QtGui.QImage.Format_ARGB32)
self._img.data = data # data must be kept alive or PySide 1.2.1 (and probably earlier) will crash.
self.update()
def paintEvent(self, ev):
@ -78,17 +93,17 @@ class RemoteGraphicsView(QtGui.QWidget):
p.end()
def mousePressEvent(self, ev):
self._view.mousePressEvent(ev.type(), ev.pos(), ev.globalPos(), ev.button(), int(ev.buttons()), int(ev.modifiers()), _callSync='off')
self._view.mousePressEvent(int(ev.type()), ev.pos(), ev.globalPos(), int(ev.button()), int(ev.buttons()), int(ev.modifiers()), _callSync='off')
ev.accept()
return QtGui.QWidget.mousePressEvent(self, ev)
def mouseReleaseEvent(self, ev):
self._view.mouseReleaseEvent(ev.type(), ev.pos(), ev.globalPos(), ev.button(), int(ev.buttons()), int(ev.modifiers()), _callSync='off')
self._view.mouseReleaseEvent(int(ev.type()), ev.pos(), ev.globalPos(), int(ev.button()), int(ev.buttons()), int(ev.modifiers()), _callSync='off')
ev.accept()
return QtGui.QWidget.mouseReleaseEvent(self, ev)
def mouseMoveEvent(self, ev):
self._view.mouseMoveEvent(ev.type(), ev.pos(), ev.globalPos(), ev.button(), int(ev.buttons()), int(ev.modifiers()), _callSync='off')
self._view.mouseMoveEvent(int(ev.type()), ev.pos(), ev.globalPos(), int(ev.button()), int(ev.buttons()), int(ev.modifiers()), _callSync='off')
ev.accept()
return QtGui.QWidget.mouseMoveEvent(self, ev)
@ -98,22 +113,27 @@ class RemoteGraphicsView(QtGui.QWidget):
return QtGui.QWidget.wheelEvent(self, ev)
def keyEvent(self, ev):
if self._view.keyEvent(ev.type(), int(ev.modifiers()), text, autorep, count):
if self._view.keyEvent(int(ev.type()), int(ev.modifiers()), text, autorep, count):
ev.accept()
return QtGui.QWidget.keyEvent(self, ev)
def enterEvent(self, ev):
self._view.enterEvent(ev.type(), _callSync='off')
self._view.enterEvent(int(ev.type()), _callSync='off')
return QtGui.QWidget.enterEvent(self, ev)
def leaveEvent(self, ev):
self._view.leaveEvent(ev.type(), _callSync='off')
self._view.leaveEvent(int(ev.type()), _callSync='off')
return QtGui.QWidget.leaveEvent(self, ev)
def remoteProcess(self):
"""Return the remote process handle. (see multiprocess.remoteproxy.RemoteEventHandler)"""
return self._proc
def close(self):
"""Close the remote process. After this call, the widget will no longer be updated."""
self._proc.close()
class Renderer(GraphicsView):
## Created by the remote process to handle render requests
@ -121,12 +141,13 @@ class Renderer(GraphicsView):
def __init__(self, *args, **kwds):
## Create shared memory for rendered image
#pg.dbg(namespace={'r': self})
if sys.platform.startswith('win'):
self.shmtag = "pyqtgraph_shmem_" + ''.join([chr((random.getrandbits(20)%25) + 97) for i in range(20)])
self.shm = mmap.mmap(-1, mmap.PAGESIZE, self.shmtag) # use anonymous mmap on windows
else:
self.shmFile = tempfile.NamedTemporaryFile(prefix='pyqtgraph_shmem_')
self.shmFile.write('\x00' * mmap.PAGESIZE)
self.shmFile.write(b'\x00' * (mmap.PAGESIZE+1))
fd = self.shmFile.fileno()
self.shm = mmap.mmap(fd, mmap.PAGESIZE, mmap.MAP_SHARED, mmap.PROT_WRITE)
atexit.register(self.close)
@ -140,9 +161,9 @@ class Renderer(GraphicsView):
def close(self):
self.shm.close()
if sys.platform.startswith('win'):
if not sys.platform.startswith('win'):
self.shmFile.close()
def shmFileName(self):
if sys.platform.startswith('win'):
return self.shmtag
@ -174,7 +195,6 @@ class Renderer(GraphicsView):
self.shm = mmap.mmap(-1, size, self.shmtag)
else:
self.shm.resize(size)
address = ctypes.addressof(ctypes.c_char.from_buffer(self.shm, 0))
## render the scene directly to shared memory
if USE_PYSIDE:
@ -182,7 +202,17 @@ class Renderer(GraphicsView):
#ch = ctypes.c_char_p(address)
self.img = QtGui.QImage(ch, self.width(), self.height(), QtGui.QImage.Format_ARGB32)
else:
self.img = QtGui.QImage(address, self.width(), self.height(), QtGui.QImage.Format_ARGB32)
address = ctypes.addressof(ctypes.c_char.from_buffer(self.shm, 0))
# different versions of pyqt have different requirements here..
try:
self.img = QtGui.QImage(sip.voidptr(address), self.width(), self.height(), QtGui.QImage.Format_ARGB32)
except TypeError:
try:
self.img = QtGui.QImage(memoryview(buffer(self.shm)), self.width(), self.height(), QtGui.QImage.Format_ARGB32)
except TypeError:
# Works on PyQt 4.9.6
self.img = QtGui.QImage(address, self.width(), self.height(), QtGui.QImage.Format_ARGB32)
self.img.fill(0xffffffff)
p = QtGui.QPainter(self.img)
self.render(p, self.viewRect(), self.rect())
@ -191,18 +221,21 @@ class Renderer(GraphicsView):
def mousePressEvent(self, typ, pos, gpos, btn, btns, mods):
typ = QtCore.QEvent.Type(typ)
btn = QtCore.Qt.MouseButton(btn)
btns = QtCore.Qt.MouseButtons(btns)
mods = QtCore.Qt.KeyboardModifiers(mods)
return GraphicsView.mousePressEvent(self, QtGui.QMouseEvent(typ, pos, gpos, btn, btns, mods))
def mouseMoveEvent(self, typ, pos, gpos, btn, btns, mods):
typ = QtCore.QEvent.Type(typ)
btn = QtCore.Qt.MouseButton(btn)
btns = QtCore.Qt.MouseButtons(btns)
mods = QtCore.Qt.KeyboardModifiers(mods)
return GraphicsView.mouseMoveEvent(self, QtGui.QMouseEvent(typ, pos, gpos, btn, btns, mods))
def mouseReleaseEvent(self, typ, pos, gpos, btn, btns, mods):
typ = QtCore.QEvent.Type(typ)
btn = QtCore.Qt.MouseButton(btn)
btns = QtCore.Qt.MouseButtons(btns)
mods = QtCore.Qt.KeyboardModifiers(mods)
return GraphicsView.mouseReleaseEvent(self, QtGui.QMouseEvent(typ, pos, gpos, btn, btns, mods))
@ -226,6 +259,3 @@ class Renderer(GraphicsView):
ev = QtCore.QEvent(QtCore.QEvent.Type(typ))
return GraphicsView.leaveEvent(self, ev)

View File

@ -6,6 +6,7 @@ import pyqtgraph.parametertree as ptree
import pyqtgraph.functions as fn
import numpy as np
from pyqtgraph.pgcollections import OrderedDict
import pyqtgraph as pg
__all__ = ['ScatterPlotWidget']
@ -47,14 +48,22 @@ class ScatterPlotWidget(QtGui.QSplitter):
self.ctrlPanel.addWidget(self.ptree)
self.addWidget(self.plot)
bg = pg.mkColor(pg.getConfigOption('background'))
bg.setAlpha(150)
self.filterText = pg.TextItem(border=pg.getConfigOption('foreground'), color=bg)
self.filterText.setPos(60,20)
self.filterText.setParentItem(self.plot.plotItem)
self.data = None
self.mouseOverField = None
self.scatterPlot = None
self.style = dict(pen=None, symbol='o')
self.fieldList.itemSelectionChanged.connect(self.fieldSelectionChanged)
self.filter.sigFilterChanged.connect(self.filterChanged)
self.colorMap.sigColorMapChanged.connect(self.updatePlot)
def setFields(self, fields):
def setFields(self, fields, mouseOverField=None):
"""
Set the list of field names/units to be processed.
@ -62,6 +71,7 @@ class ScatterPlotWidget(QtGui.QSplitter):
:func:`ColorMapWidget.setFields <pyqtgraph.widgets.ColorMapWidget.ColorMapParameter.setFields>`
"""
self.fields = OrderedDict(fields)
self.mouseOverField = mouseOverField
self.fieldList.clear()
for f,opts in fields:
item = QtGui.QListWidgetItem(f)
@ -94,6 +104,13 @@ class ScatterPlotWidget(QtGui.QSplitter):
def filterChanged(self, f):
self.filtered = None
self.updatePlot()
desc = self.filter.describe()
if len(desc) == 0:
self.filterText.setVisible(False)
else:
self.filterText.setText('\n'.join(desc))
self.filterText.setVisible(True)
def updatePlot(self):
self.plot.clear()
@ -122,64 +139,78 @@ class ScatterPlotWidget(QtGui.QSplitter):
self.plot.setLabels(left=('N', ''), bottom=(sel[0], units[0]), title='')
if len(data) == 0:
return
x = data[sel[0]]
#if x.dtype.kind == 'f':
#mask = ~np.isnan(x)
#else:
#mask = np.ones(len(x), dtype=bool)
#x = x[mask]
#style['symbolBrush'] = colors[mask]
y = None
#x = data[sel[0]]
#y = None
xy = [data[sel[0]], None]
elif len(sel) == 2:
self.plot.setLabels(left=(sel[1],units[1]), bottom=(sel[0],units[0]))
if len(data) == 0:
return
xydata = []
for ax in [0,1]:
d = data[sel[ax]]
## scatter catecorical values just a bit so they show up better in the scatter plot.
#if sel[ax] in ['MorphologyBSMean', 'MorphologyTDMean', 'FIType']:
#d += np.random.normal(size=len(cells), scale=0.1)
xydata.append(d)
x,y = xydata
#mask = np.ones(len(x), dtype=bool)
#if x.dtype.kind == 'f':
#mask |= ~np.isnan(x)
#if y.dtype.kind == 'f':
#mask |= ~np.isnan(y)
#x = x[mask]
#y = y[mask]
#style['symbolBrush'] = colors[mask]
xy = [data[sel[0]], data[sel[1]]]
#xydata = []
#for ax in [0,1]:
#d = data[sel[ax]]
### scatter catecorical values just a bit so they show up better in the scatter plot.
##if sel[ax] in ['MorphologyBSMean', 'MorphologyTDMean', 'FIType']:
##d += np.random.normal(size=len(cells), scale=0.1)
#xydata.append(d)
#x,y = xydata
## convert enum-type fields to float, set axis labels
xy = [x,y]
enum = [False, False]
for i in [0,1]:
axis = self.plot.getAxis(['bottom', 'left'][i])
if xy[i] is not None and xy[i].dtype.kind in ('S', 'O'):
if xy[i] is not None and (self.fields[sel[i]].get('mode', None) == 'enum' or xy[i].dtype.kind in ('S', 'O')):
vals = self.fields[sel[i]].get('values', list(set(xy[i])))
xy[i] = np.array([vals.index(x) if x in vals else None for x in xy[i]], dtype=float)
xy[i] = np.array([vals.index(x) if x in vals else len(vals) for x in xy[i]], dtype=float)
axis.setTicks([list(enumerate(vals))])
enum[i] = True
else:
axis.setTicks(None) # reset to automatic ticking
x,y = xy
## mask out any nan values
mask = np.ones(len(x), dtype=bool)
if x.dtype.kind == 'f':
mask &= ~np.isnan(x)
if y is not None and y.dtype.kind == 'f':
mask &= ~np.isnan(y)
x = x[mask]
mask = np.ones(len(xy[0]), dtype=bool)
if xy[0].dtype.kind == 'f':
mask &= ~np.isnan(xy[0])
if xy[1] is not None and xy[1].dtype.kind == 'f':
mask &= ~np.isnan(xy[1])
xy[0] = xy[0][mask]
style['symbolBrush'] = colors[mask]
## Scatter y-values for a histogram-like appearance
if y is None:
y = fn.pseudoScatter(x)
if xy[1] is None:
## column scatter plot
xy[1] = fn.pseudoScatter(xy[0])
else:
y = y[mask]
self.plot.plot(x, y, **style)
## beeswarm plots
xy[1] = xy[1][mask]
for ax in [0,1]:
if not enum[ax]:
continue
imax = int(xy[ax].max()) if len(xy[ax]) > 0 else 0
for i in range(imax+1):
keymask = xy[ax] == i
scatter = pg.pseudoScatter(xy[1-ax][keymask], bidir=True)
if len(scatter) == 0:
continue
smax = np.abs(scatter).max()
if smax != 0:
scatter *= 0.2 / smax
xy[ax][keymask] += scatter
if self.scatterPlot is not None:
try:
self.scatterPlot.sigPointsClicked.disconnect(self.plotClicked)
except:
pass
self.scatterPlot = self.plot.plot(xy[0], xy[1], data=data[mask], **style)
self.scatterPlot.sigPointsClicked.connect(self.plotClicked)
def plotClicked(self, plot, points):
pass

View File

@ -313,7 +313,7 @@ class SpinBox(QtGui.QAbstractSpinBox):
s = [D(-1), D(1)][n >= 0] ## determine sign of step
val = self.val
for i in range(abs(n)):
for i in range(int(abs(n))):
if self.opts['log']:
raise Exception("Log mode no longer supported.")

View File

@ -6,27 +6,26 @@ import numpy as np
try:
import metaarray
HAVE_METAARRAY = True
except:
except ImportError:
HAVE_METAARRAY = False
__all__ = ['TableWidget']
class TableWidget(QtGui.QTableWidget):
"""Extends QTableWidget with some useful functions for automatic data handling
and copy / export context menu. Can automatically format and display:
- numpy arrays
- numpy record arrays
- metaarrays
- list-of-lists [[1,2,3], [4,5,6]]
- dict-of-lists {'x': [1,2,3], 'y': [4,5,6]}
- list-of-dicts [{'x': 1, 'y': 4}, {'x': 2, 'y': 5}, ...]
and copy / export context menu. Can automatically format and display a variety
of data types (see :func:`setData() <pyqtgraph.TableWidget.setData>` for more
information.
"""
def __init__(self, *args):
def __init__(self, *args, **kwds):
QtGui.QTableWidget.__init__(self, *args)
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)
self.contextMenu = QtGui.QMenu()
self.contextMenu.addAction('Copy Selection').triggered.connect(self.copySel)
self.contextMenu.addAction('Copy All').triggered.connect(self.copyAll)
@ -34,6 +33,7 @@ class TableWidget(QtGui.QTableWidget):
self.contextMenu.addAction('Save All').triggered.connect(self.saveAll)
def clear(self):
"""Clear all contents from the table."""
QtGui.QTableWidget.clear(self)
self.verticalHeadersSet = False
self.horizontalHeadersSet = False
@ -42,8 +42,19 @@ class TableWidget(QtGui.QTableWidget):
self.setColumnCount(0)
def setData(self, data):
"""Set the data displayed in the table.
Allowed formats are:
* numpy arrays
* numpy record arrays
* metaarrays
* list-of-lists [[1,2,3], [4,5,6]]
* dict-of-lists {'x': [1,2,3], 'y': [4,5,6]}
* list-of-dicts [{'x': 1, 'y': 4}, {'x': 2, 'y': 5}, ...]
"""
self.clear()
self.appendData(data)
self.resizeColumnsToContents()
def appendData(self, data):
"""Types allowed:
@ -60,26 +71,19 @@ class TableWidget(QtGui.QTableWidget):
first = next(it0)
except StopIteration:
return
#if type(first) == type(np.float64(1)):
# return
fn1, header1 = self.iteratorFn(first)
if fn1 is None:
self.clear()
return
#print fn0, header0
#print fn1, header1
firstVals = [x for x in fn1(first)]
self.setColumnCount(len(firstVals))
#print header0, header1
if not self.verticalHeadersSet and header0 is not None:
#print "set header 0:", header0
self.setRowCount(len(header0))
self.setVerticalHeaderLabels(header0)
self.verticalHeadersSet = True
if not self.horizontalHeadersSet and header1 is not None:
#print "set header 1:", header1
self.setHorizontalHeaderLabels(header1)
self.horizontalHeadersSet = True
@ -88,10 +92,15 @@ class TableWidget(QtGui.QTableWidget):
for row in it0:
self.setRow(i, [x for x in fn1(row)])
i += 1
def setEditable(self, editable=True):
self.editable = editable
for item in self.items:
item.setEditable(editable)
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):
## 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()))
@ -110,13 +119,16 @@ class TableWidget(QtGui.QTableWidget):
elif data is None:
return (None,None)
else:
raise Exception("Don't know how to iterate over data type: %s" % str(type(data)))
msg = "Don't know how to iterate over data type: {!s}".format(type(data))
raise TypeError(msg)
def iterFirstAxis(self, data):
for i in range(data.shape[0]):
yield data[i]
def iterate(self, data): ## for numpy.void, which can be iterated but mysteriously has no __iter__ (??)
def iterate(self, data):
# for numpy.void, which can be iterated but mysteriously
# has no __iter__ (??)
for x in data:
yield x
@ -124,32 +136,39 @@ class TableWidget(QtGui.QTableWidget):
self.appendData([data])
def addRow(self, vals):
#print "add row:", vals
row = self.rowCount()
self.setRowCount(row+1)
self.setRowCount(row + 1)
self.setRow(row, vals)
def setRow(self, row, vals):
if row > self.rowCount()-1:
self.setRowCount(row+1)
for col in range(self.columnCount()):
if row > self.rowCount() - 1:
self.setRowCount(row + 1)
for col in range(len(vals)):
val = vals[col]
if isinstance(val, float) or isinstance(val, np.floating):
s = "%0.3g" % val
else:
s = str(val)
item = QtGui.QTableWidgetItem(s)
item.value = val
#print "add item to row %d:"%row, item, item.value
item = TableWidgetItem(val)
item.setEditable(self.editable)
self.items.append(item)
self.setItem(row, col, item)
def sizeHint(self):
# based on http://stackoverflow.com/a/7195443/54056
width = sum(self.columnWidth(i) for i in range(self.columnCount()))
width += self.verticalHeader().sizeHint().width()
width += self.verticalScrollBar().sizeHint().width()
width += self.frameWidth() * 2
height = sum(self.rowHeight(i) for i in range(self.rowCount()))
height += self.verticalHeader().sizeHint().height()
height += self.horizontalScrollBar().sizeHint().height()
return QtCore.QSize(width, height)
def serialize(self, useSelection=False):
"""Convert entire table (or just selected area) into tab-separated text values"""
if useSelection:
selection = self.selectedRanges()[0]
rows = list(range(selection.topRow(), selection.bottomRow()+1))
columns = list(range(selection.leftColumn(), selection.rightColumn()+1))
rows = list(range(selection.topRow(),
selection.bottomRow() + 1))
columns = list(range(selection.leftColumn(),
selection.rightColumn() + 1))
else:
rows = list(range(self.rowCount()))
columns = list(range(self.columnCount()))
@ -215,6 +234,28 @@ class TableWidget(QtGui.QTableWidget):
else:
ev.ignore()
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
flags = QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled
self.setFlags(flags)
def setEditable(self, editable):
if editable:
self.setFlags(self.flags() | QtCore.Qt.ItemIsEditable)
else:
self.setFlags(self.flags() & ~QtCore.Qt.ItemIsEditable)
def __lt__(self, other):
if hasattr(other, 'value'):
return self.value < other.value
else:
return self.text() < other.text()
if __name__ == '__main__':