Merge tag 'pyqtgraph-0.9.8' into pyqtgraph-core
This commit is contained in:
commit
757dc50447
@ -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 = {}
|
||||
|
6
Point.py
6
Point.py
@ -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)
|
||||
|
||||
|
@ -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))
|
||||
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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):
|
||||
@ -34,6 +34,12 @@ 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()))
|
||||
|
||||
|
63
__init__.py
63
__init__.py
@ -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
|
||||
|
@ -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
|
||||
|
9
debug.py
9
debug.py
@ -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 = []
|
||||
|
@ -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())
|
||||
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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:
|
||||
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()
|
||||
|
@ -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 = """\
|
||||
@ -222,7 +223,7 @@ def _generateItemSvg(item, nodes=None, root=None):
|
||||
#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 = ''
|
||||
@ -375,7 +405,6 @@ def correctCoordinates(node, item):
|
||||
if removeTransform:
|
||||
grp.removeAttribute('transform')
|
||||
|
||||
|
||||
def itemTransform(item, root):
|
||||
## Return the transformation mapping item to root
|
||||
## (actually to parent coordinate system of root)
|
||||
|
@ -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:
|
||||
|
@ -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)
|
||||
|
@ -24,6 +24,14 @@ class BinOpNode(Node):
|
||||
})
|
||||
|
||||
def process(self, **args):
|
||||
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:
|
||||
@ -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__'))
|
||||
|
||||
|
||||
|
165
functions.py
165
functions.py
@ -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')
|
||||
if pg.getConfigOption('useWeave'):
|
||||
try:
|
||||
import scipy.weave
|
||||
USE_WEAVE = getConfigOption('useWeave')
|
||||
except:
|
||||
USE_WEAVE = False
|
||||
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:
|
||||
@ -1061,8 +1082,23 @@ def arrayToQPath(x, y, connect='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])
|
||||
#if connect == 'all':
|
||||
#for i in range(1, y.shape[0]):
|
||||
# path.lineTo(x[i], y[i])
|
||||
#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:
|
||||
@ -1077,13 +1113,14 @@ def arrayToQPath(x, y, connect='all'):
|
||||
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)
|
||||
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
|
||||
@ -1095,7 +1132,9 @@ def arrayToQPath(x, y, connect='all'):
|
||||
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):
|
||||
@ -1106,37 +1145,25 @@ def arrayToQPath(x, y, connect='all'):
|
||||
#prof.mark('fill array')
|
||||
# write last 0
|
||||
lastInd = 20*(n+1)
|
||||
arr.data[lastInd:lastInd+4] = struct.pack('>i', 0)
|
||||
byteview.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
|
||||
|
||||
## 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)
|
||||
#prof.mark('create datastream')
|
||||
|
||||
ds >> path
|
||||
#prof.mark('load')
|
||||
|
||||
#prof.finish()
|
||||
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')
|
||||
|
||||
return path
|
||||
|
||||
@ -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
|
||||
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
|
||||
limits = limits[:,mask]
|
||||
else:
|
||||
mask = limits[0] <= y
|
||||
|
||||
limits2 = limits[:,mask]
|
||||
|
||||
# are we inside an excluded region?
|
||||
mask = (limits[0] < y) & (limits[1] > y)
|
||||
mask = (limits2[0] < y) & (limits2[1] > y)
|
||||
if mask.sum() == 0:
|
||||
break
|
||||
y = limits[:,mask].max()
|
||||
|
||||
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
|
||||
|
@ -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.
|
||||
"""
|
||||
# Deprecated usage, kept for backward compatibility
|
||||
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
|
||||
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,11 +368,22 @@ 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():
|
||||
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):
|
||||
linkedView = self.linkedView()
|
||||
@ -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
|
||||
@ -459,6 +531,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)
|
||||
allValues = np.array([])
|
||||
@ -470,17 +546,26 @@ 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
|
||||
|
||||
def logTickValues(self, minVal, maxVal, size, stdTicks):
|
||||
@ -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,6 +691,10 @@ class AxisItem(GraphicsWidget):
|
||||
|
||||
## determine mapping between tick values and local coordinates
|
||||
dif = self.range[1] - self.range[0]
|
||||
if dif == 0:
|
||||
xscale = 1
|
||||
offset = 0
|
||||
else:
|
||||
if axis == 0:
|
||||
xScale = -bounds.height() / dif
|
||||
offset = self.range[0] * xScale - bounds.height()
|
||||
@ -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)
|
||||
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())
|
||||
p.drawText(rect, textFlags, vstr)
|
||||
for rect, flags, text in textSpecs:
|
||||
p.drawText(rect, flags, text)
|
||||
#p.drawRect(rect)
|
||||
|
||||
prof.mark('draw text')
|
||||
prof.finish()
|
||||
|
||||
|
149
graphicsItems/BarGraphItem.py
Normal file
149
graphicsItems/BarGraphItem.py
Normal 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())
|
||||
|
||||
|
@ -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()
|
||||
|
||||
|
||||
|
||||
|
@ -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
|
||||
|
||||
|
@ -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:
|
||||
|
@ -20,15 +20,16 @@ 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
|
||||
|
||||
#def getMenu(self):
|
||||
#pass
|
||||
# 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 setFixedHeight(self, h):
|
||||
self.setMaximumHeight(h)
|
||||
|
@ -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.
|
||||
|
||||
"""
|
||||
|
||||
@ -46,6 +48,51 @@ class GraphicsWidgetAnchor(object):
|
||||
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
|
||||
|
@ -196,9 +196,11 @@ 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')
|
||||
|
||||
@ -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)
|
||||
|
@ -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,
|
||||
|
@ -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,11 +63,16 @@ 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)
|
||||
if isinstance(item, ItemSample):
|
||||
sample = item
|
||||
else:
|
||||
sample = ItemSample(item)
|
||||
row = len(self.items)
|
||||
self.items.append((sample, label))
|
||||
@ -74,6 +80,26 @@ class LegendItem(GraphicsWidget, GraphicsWidgetAnchor):
|
||||
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
|
||||
@ -96,8 +122,20 @@ class LegendItem(GraphicsWidget, GraphicsWidgetAnchor):
|
||||
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,9 +152,22 @@ class ItemSample(GraphicsWidget):
|
||||
p.setPen(fn.mkPen(None))
|
||||
p.drawPolygon(QtGui.QPolygonF([QtCore.QPointF(2,18), QtCore.QPointF(18,2), QtCore.QPointF(18,18)]))
|
||||
|
||||
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)
|
||||
|
||||
|
||||
|
||||
|
||||
|
@ -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,15 +110,20 @@ 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:
|
||||
@ -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
|
||||
|
@ -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.
|
||||
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*
|
||||
decimate (int) sub-sample data by selecting every nth sample before plotting
|
||||
========== =====================================================================
|
||||
================ =====================================================================
|
||||
|
||||
**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:
|
||||
return
|
||||
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['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.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
|
||||
|
||||
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
|
||||
|
||||
|
||||
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]
|
||||
#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()
|
||||
@ -483,33 +597,6 @@ class PlotDataItem(GraphicsObject):
|
||||
]
|
||||
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):
|
||||
"""
|
||||
Return the size in pixels that this item may draw beyond the values returned by dataBounds().
|
||||
@ -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:
|
||||
|
@ -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,18 +300,20 @@ 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.
|
||||
|
||||
"""
|
||||
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():
|
||||
if self.ctrl.downsampleCheck.isChecked():
|
||||
ds = self.ctrl.downsampleSpin.value()
|
||||
else:
|
||||
ds = True
|
||||
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):
|
||||
"""
|
||||
|
@ -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 "Max Traces" 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">
|
||||
|
@ -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))
|
||||
|
@ -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))
|
||||
|
@ -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,7 +835,10 @@ class ROI(GraphicsObject):
|
||||
#print " dshape", dShape
|
||||
|
||||
## Determine transform that maps ROI bounding box to image coordinates
|
||||
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()
|
||||
@ -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,12 +1725,35 @@ 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
|
||||
|
||||
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()
|
||||
|
@ -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)
|
||||
|
||||
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 parentChanged(self):
|
||||
view = self.parentItem()
|
||||
if view is None:
|
||||
return
|
||||
view.sigRangeChanged.connect(self.updateBar)
|
||||
self.updateBar()
|
||||
|
||||
|
||||
def setSize(self, s):
|
||||
self.size = s
|
||||
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
|
||||
|
||||
|
@ -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,12 +625,14 @@ 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]
|
||||
@ -690,6 +688,7 @@ class ScatterPlotItem(GraphicsObject):
|
||||
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):
|
||||
|
@ -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,9 +401,9 @@ 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.
|
||||
@ -349,73 +411,118 @@ class ViewBox(GraphicsWidget):
|
||||
*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)
|
||||
# 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)
|
||||
|
||||
# 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)
|
||||
|
||||
if update:
|
||||
self.updateMatrix(changed)
|
||||
# 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)
|
||||
|
||||
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).
|
||||
"""
|
||||
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
|
||||
|
||||
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):
|
||||
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()
|
||||
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).
|
||||
@ -508,24 +651,46 @@ class ViewBox(GraphicsWidget):
|
||||
#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:
|
||||
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,6 +828,7 @@ 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()
|
||||
@ -668,6 +836,7 @@ class ViewBox(GraphicsWidget):
|
||||
if self.autoRangeEnabled()[axis] is False:
|
||||
slot()
|
||||
|
||||
|
||||
self.sigStateChanged.emit(self)
|
||||
|
||||
def blockLink(self, b):
|
||||
@ -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()
|
||||
@ -724,7 +894,10 @@ class ViewBox(GraphicsWidget):
|
||||
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 updateMatrix(self, changed=None):
|
||||
## Make the childGroup's transform match the requested range.
|
||||
|
||||
if changed is None:
|
||||
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]
|
||||
changed = list(changed)
|
||||
#print "udpateMatrix:"
|
||||
#print " range:", self.range
|
||||
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][:]]
|
||||
# Make correction for aspect ratio constraint
|
||||
|
||||
## aspect is (widget w/h) / (view range w/h)
|
||||
aspect = self.state['aspectLocked'] # size ratio / view ratio
|
||||
tr = self.targetRect()
|
||||
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:
|
||||
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())
|
||||
# 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,22 +1430,24 @@ 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']
|
||||
if bg is None:
|
||||
@ -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):
|
||||
"""
|
||||
|
@ -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)
|
||||
|
95
graphicsItems/tests/ViewBox.py
Normal file
95
graphicsItems/tests/ViewBox.py
Normal 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()
|
@ -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
|
||||
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.
|
||||
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};
|
||||
|
||||
*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*
|
||||
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:
|
||||
@ -267,12 +242,11 @@ 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,16 +302,10 @@ 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."""
|
||||
self.ui.histogram.setLevels(min, max)
|
||||
@ -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
|
||||
|
@ -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()
|
||||
|
@ -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)
|
||||
|
@ -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.
|
||||
|
@ -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))
|
||||
|
||||
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()
|
||||
@ -116,6 +141,7 @@ class Process(RemoteEventHandler):
|
||||
|
||||
atexit.register(self.join)
|
||||
|
||||
|
||||
def join(self, timeout=10):
|
||||
self.debugMsg('Joining child process..')
|
||||
if self.proc.poll() is None:
|
||||
@ -127,9 +153,23 @@ class Process(RemoteEventHandler):
|
||||
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,17 +379,28 @@ 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()
|
||||
if self._processRequests:
|
||||
app = QtGui.QApplication.instance()
|
||||
if app is None:
|
||||
raise Exception("Must create QApplication before starting QtProcess")
|
||||
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:
|
||||
@ -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)
|
||||
|
||||
|
||||
|
||||
|
@ -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.
|
||||
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.
|
||||
|
@ -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()
|
||||
|
@ -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)
|
||||
|
||||
def paintGL(self):
|
||||
self.setProjection()
|
||||
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, 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()
|
||||
|
||||
@ -287,3 +353,84 @@ class GLViewWidget(QtOpenGL.QGLWidget):
|
||||
|
||||
|
||||
|
||||
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
|
||||
|
||||
|
||||
|
||||
|
@ -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)
|
||||
|
||||
@ -144,11 +144,18 @@ class MeshData(object):
|
||||
"""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()
|
||||
@ -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
|
||||
|
@ -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
|
||||
|
@ -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()
|
||||
|
29
opengl/items/GLBarGraphItem.py
Normal file
29
opengl/items/GLBarGraphItem.py
Normal 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)
|
||||
|
||||
|
||||
|
@ -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())
|
||||
|
@ -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()
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
||||
|
||||
|
@ -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,6 +162,7 @@ class GLMeshItem(GLGraphicsItem):
|
||||
|
||||
self.parseMeshData()
|
||||
|
||||
if self.opts['drawFaces']:
|
||||
with self.shader():
|
||||
verts = self.vertexes
|
||||
norms = self.normals
|
||||
@ -180,3 +199,25 @@ class GLMeshItem(GLGraphicsItem):
|
||||
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.edgeColors is None:
|
||||
color = self.opts['edgeColor']
|
||||
if isinstance(color, QtGui.QColor):
|
||||
glColor4f(*pg.glColor(color))
|
||||
else:
|
||||
glColor4f(*color)
|
||||
else:
|
||||
glEnableClientState(GL_COLOR_ARRAY)
|
||||
glColorPointerf(color)
|
||||
edges = edges.flatten()
|
||||
glDrawElements(GL_LINES, edges.shape[0], GL_UNSIGNED_INT, edges)
|
||||
finally:
|
||||
glDisableClientState(GL_VERTEX_ARRAY)
|
||||
glDisableClientState(GL_COLOR_ARRAY)
|
||||
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
127
ordereddict.py
Normal file
127
ordereddict.py
Normal 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
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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()
|
||||
|
@ -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):
|
||||
|
@ -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)
|
||||
|
@ -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,7 +72,7 @@ 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]
|
||||
@ -78,10 +80,20 @@ class DataFilterParameter(ptree.types.GroupParameter):
|
||||
#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))
|
@ -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,9 +139,6 @@ 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)
|
||||
|
||||
@ -149,6 +147,11 @@ class GraphicsView(QtGui.QGraphicsView):
|
||||
#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,6 +184,7 @@ class GraphicsView(QtGui.QGraphicsView):
|
||||
if self.centralWidget is not None:
|
||||
self.scene().removeItem(self.centralWidget)
|
||||
self.centralWidget = item
|
||||
if item is not None:
|
||||
self.sceneObj.addItem(item)
|
||||
self.resizeEvent(None)
|
||||
|
||||
@ -272,6 +276,7 @@ class GraphicsView(QtGui.QGraphicsView):
|
||||
scaleChanged = True
|
||||
self.range = newRect
|
||||
#print "New Range:", self.range
|
||||
if self.centralWidget is not None:
|
||||
self.centralWidget.setGeometry(self.range)
|
||||
self.updateMatrix(propagate)
|
||||
if scaleChanged:
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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:
|
||||
if self.opts is None:
|
||||
return
|
||||
p = QtGui.QPainter(self)
|
||||
p.drawImage(self.rect(), self.image)
|
||||
p.end()
|
||||
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)
|
||||
|
||||
|
||||
|
||||
|
@ -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,7 +161,7 @@ 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):
|
||||
@ -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,6 +202,16 @@ class Renderer(GraphicsView):
|
||||
#ch = ctypes.c_char_p(address)
|
||||
self.img = QtGui.QImage(ch, self.width(), self.height(), QtGui.QImage.Format_ARGB32)
|
||||
else:
|
||||
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)
|
||||
@ -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)
|
||||
|
||||
|
||||
|
||||
|
||||
|
@ -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]
|
||||
## 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)
|
||||
|
||||
|
||||
self.plot.plot(x, y, **style)
|
||||
def plotClicked(self, plot, points):
|
||||
pass
|
||||
|
||||
|
||||
|
@ -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.")
|
||||
|
@ -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
|
||||
|
||||
@ -89,9 +93,14 @@ class TableWidget(QtGui.QTableWidget):
|
||||
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__':
|
||||
|
Loading…
Reference in New Issue
Block a user