merge from luke

This commit is contained in:
Megan Kratz 2012-10-26 15:50:04 -04:00
commit 885d2157f1
32 changed files with 1155 additions and 324 deletions

View File

@ -77,6 +77,11 @@ class Dock(QtGui.QWidget, DockDrop):
return name == 'dock' return name == 'dock'
def setStretch(self, x=None, y=None): def setStretch(self, x=None, y=None):
"""
Set the 'target' size for this Dock.
The actual size will be determined by comparing this Dock's
stretch value to the rest of the docks it shares space with.
"""
#print "setStretch", self, x, y #print "setStretch", self, x, y
#self._stretch = (x, y) #self._stretch = (x, y)
if x is None: if x is None:
@ -100,6 +105,10 @@ class Dock(QtGui.QWidget, DockDrop):
#return self._stretch #return self._stretch
def hideTitleBar(self): def hideTitleBar(self):
"""
Hide the title bar for this Dock.
This will prevent the Dock being moved by the user.
"""
self.label.hide() self.label.hide()
self.labelHidden = True self.labelHidden = True
if 'center' in self.allowedAreas: if 'center' in self.allowedAreas:
@ -107,12 +116,21 @@ class Dock(QtGui.QWidget, DockDrop):
self.updateStyle() self.updateStyle()
def showTitleBar(self): def showTitleBar(self):
"""
Show the title bar for this Dock.
"""
self.label.show() self.label.show()
self.labelHidden = False self.labelHidden = False
self.allowedAreas.add('center') self.allowedAreas.add('center')
self.updateStyle() self.updateStyle()
def setOrientation(self, o='auto', force=False): def setOrientation(self, o='auto', force=False):
"""
Sets the orientation of the title bar for this Dock.
Must be one of 'auto', 'horizontal', or 'vertical'.
By default ('auto'), the orientation is determined
based on the aspect ratio of the Dock.
"""
#print self.name(), "setOrientation", o, force #print self.name(), "setOrientation", o, force
if o == 'auto' and self.autoOrient: if o == 'auto' and self.autoOrient:
if self.container().type() == 'tab': if self.container().type() == 'tab':
@ -127,6 +145,7 @@ class Dock(QtGui.QWidget, DockDrop):
self.updateStyle() self.updateStyle()
def updateStyle(self): def updateStyle(self):
## updates orientation and appearance of title bar
#print self.name(), "update style:", self.orientation, self.moveLabel, self.label.isVisible() #print self.name(), "update style:", self.orientation, self.moveLabel, self.label.isVisible()
if self.labelHidden: if self.labelHidden:
self.widgetArea.setStyleSheet(self.nStyle) self.widgetArea.setStyleSheet(self.nStyle)
@ -154,6 +173,10 @@ class Dock(QtGui.QWidget, DockDrop):
return self._container return self._container
def addWidget(self, widget, row=None, col=0, rowspan=1, colspan=1): def addWidget(self, widget, row=None, col=0, rowspan=1, colspan=1):
"""
Add a new widget to the interior of this Dock.
Each Dock uses a QGridLayout to arrange widgets within.
"""
if row is None: if row is None:
row = self.currentRow row = self.currentRow
self.currentRow = max(row+1, self.currentRow) self.currentRow = max(row+1, self.currentRow)
@ -188,7 +211,8 @@ class Dock(QtGui.QWidget, DockDrop):
def __repr__(self): def __repr__(self):
return "<Dock %s %s>" % (self.name(), self.stretch()) return "<Dock %s %s>" % (self.name(), self.stretch())
class DockLabel(VerticalLabel): class DockLabel(VerticalLabel):
sigClicked = QtCore.Signal(object, object) sigClicked = QtCore.Signal(object, object)
@ -287,76 +311,3 @@ class DockLabel(VerticalLabel):
#class DockLabel(QtGui.QWidget):
#def __init__(self, text, dock):
#QtGui.QWidget.__init__(self)
#self._text = text
#self.dock = dock
#self.orientation = None
#self.setOrientation('horizontal')
#def text(self):
#return self._text
#def mousePressEvent(self, ev):
#if ev.button() == QtCore.Qt.LeftButton:
#self.pressPos = ev.pos()
#self.startedDrag = False
#ev.accept()
#def mouseMoveEvent(self, ev):
#if not self.startedDrag and (ev.pos() - self.pressPos).manhattanLength() > QtGui.QApplication.startDragDistance():
#self.dock.startDrag()
#ev.accept()
##print ev.pos()
#def mouseReleaseEvent(self, ev):
#ev.accept()
#def mouseDoubleClickEvent(self, ev):
#if ev.button() == QtCore.Qt.LeftButton:
#self.dock.float()
#def setOrientation(self, o):
#if self.orientation == o:
#return
#self.orientation = o
#self.update()
#self.updateGeometry()
#def paintEvent(self, ev):
#p = QtGui.QPainter(self)
#p.setBrush(QtGui.QBrush(QtGui.QColor(100, 100, 200)))
#p.setPen(QtGui.QPen(QtGui.QColor(50, 50, 100)))
#p.drawRect(self.rect().adjusted(0, 0, -1, -1))
#p.setPen(QtGui.QPen(QtGui.QColor(255, 255, 255)))
#if self.orientation == 'vertical':
#p.rotate(-90)
#rgn = QtCore.QRect(-self.height(), 0, self.height(), self.width())
#else:
#rgn = self.rect()
#align = QtCore.Qt.AlignTop|QtCore.Qt.AlignHCenter
#self.hint = p.drawText(rgn, align, self.text())
#p.end()
#if self.orientation == 'vertical':
#self.setMaximumWidth(self.hint.height())
#self.setMaximumHeight(16777215)
#else:
#self.setMaximumHeight(self.hint.height())
#self.setMaximumWidth(16777215)
#def sizeHint(self):
#if self.orientation == 'vertical':
#if hasattr(self, 'hint'):
#return QtCore.QSize(self.hint.height(), self.hint.width())
#else:
#return QtCore.QSize(19, 50)
#else:
#if hasattr(self, 'hint'):
#return QtCore.QSize(self.hint.width(), self.hint.height())
#else:
#return QtCore.QSize(50, 19)

View File

@ -35,8 +35,18 @@ class DockArea(Container, QtGui.QWidget, DockDrop):
def addDock(self, dock, position='bottom', relativeTo=None): def addDock(self, dock, position='bottom', relativeTo=None):
"""Adds a dock to this area. """Adds a dock to this area.
position may be: bottom, top, left, right, over, under
If relativeTo specifies an existing dock, the new dock is added adjacent to it""" =========== =================================================================
Arguments:
dock The new Dock object to add.
position 'bottom', 'top', 'left', 'right', 'over', or 'under'
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').
=========== =================================================================
"""
## Determine the container to insert this dock into. ## Determine the container to insert this dock into.
## If there is no neighbor, then the container is the top. ## If there is no neighbor, then the container is the top.
@ -90,6 +100,17 @@ class DockArea(Container, QtGui.QWidget, DockDrop):
dock.area = self dock.area = self
self.docks[dock.name()] = dock self.docks[dock.name()] = dock
def moveDock(self, dock, position, neighbor):
"""
Move an existing Dock to a new location.
"""
old = dock.container()
## Moving to the edge of a tabbed dock causes a drop outside the tab box
if position in ['left', 'right', 'top', 'bottom'] and neighbor is not None and neighbor.container() is not None and neighbor.container().type() == 'tab':
neighbor = neighbor.container()
self.addDock(dock, position, neighbor)
old.apoptose()
def getContainer(self, obj): def getContainer(self, obj):
if obj is None: if obj is None:
return self return self
@ -131,13 +152,6 @@ class DockArea(Container, QtGui.QWidget, DockDrop):
return 0 return 0
return 1 return 1
def moveDock(self, dock, position, neighbor):
old = dock.container()
## Moving to the edge of a tabbed dock causes a drop outside the tab box
if position in ['left', 'right', 'top', 'bottom'] and neighbor is not None and neighbor.container() is not None and neighbor.container().type() == 'tab':
neighbor = neighbor.container()
self.addDock(dock, position, neighbor)
old.apoptose()
#def paintEvent(self, ev): #def paintEvent(self, ev):
#self.drawDockOverlay() #self.drawDockOverlay()
@ -159,6 +173,7 @@ class DockArea(Container, QtGui.QWidget, DockDrop):
return area return area
def floatDock(self, dock): def floatDock(self, dock):
"""Removes *dock* from this DockArea and places it in a new window."""
area = self.addTempArea() area = self.addTempArea()
area.win.resize(dock.size()) area.win.resize(dock.size())
area.moveDock(dock, 'top', None) area.moveDock(dock, 'top', None)
@ -170,6 +185,9 @@ class DockArea(Container, QtGui.QWidget, DockDrop):
area.window().close() area.window().close()
def saveState(self): def saveState(self):
"""
Return a serialized (storable) representation of the state of
all Docks in this DockArea."""
state = {'main': self.childState(self.topContainer), 'float': []} state = {'main': self.childState(self.topContainer), 'float': []}
for a in self.tempAreas: for a in self.tempAreas:
geo = a.win.geometry() geo = a.win.geometry()
@ -188,6 +206,10 @@ class DockArea(Container, QtGui.QWidget, DockDrop):
def restoreState(self, state): def restoreState(self, state):
"""
Restore Dock configuration as generated by saveState.
"""
## 1) make dict of all docks and list of existing containers ## 1) make dict of all docks and list of existing containers
containers, docks = self.findAll() containers, docks = self.findAll()
oldTemps = self.tempAreas[:] oldTemps = self.tempAreas[:]

View File

@ -0,0 +1,8 @@
GLImageItem
===========
.. autoclass:: pyqtgraph.opengl.GLImageItem
:members:
.. automethod:: pyqtgraph.opengl.GLImageItem.__init__

View File

@ -0,0 +1,8 @@
GLScatterPlotItem
=================
.. autoclass:: pyqtgraph.opengl.GLScatterPlotItem
:members:
.. automethod:: pyqtgraph.opengl.GLScatterPlotItem.__init__

View File

@ -18,7 +18,9 @@ Contents:
glgriditem glgriditem
glmeshitem glmeshitem
glvolumeitem glvolumeitem
glimageitem
glaxisitem glaxisitem
glgraphicsitem glgraphicsitem
glscatterplotitem
meshdata meshdata

View File

@ -29,6 +29,7 @@ Contents:
scalebar scalebar
labelitem labelitem
vtickgroup vtickgroup
legenditem
gradienteditoritem gradienteditoritem
histogramlutitem histogramlutitem
gradientlegend gradientlegend

View File

@ -0,0 +1,8 @@
LegendItem
==========
.. autoclass:: pyqtgraph.LegendItem
:members:
.. automethod:: pyqtgraph.LegendItem.__init__

47
examples/GLImageItem.py Normal file
View File

@ -0,0 +1,47 @@
# -*- coding: utf-8 -*-
## Add path to library (just for examples; you do not need this)
import sys, os
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', '..'))
from pyqtgraph.Qt import QtCore, QtGui
import pyqtgraph.opengl as gl
import pyqtgraph as pg
import numpy as np
import scipy.ndimage as ndi
app = QtGui.QApplication([])
w = gl.GLViewWidget()
w.opts['distance'] = 200
w.show()
## create volume data set to slice three images from
shape = (100,100,70)
data = ndi.gaussian_filter(np.random.normal(size=shape), (4,4,4))
data += ndi.gaussian_filter(np.random.normal(size=shape), (15,15,15))*15
## slice out three planes, convert to ARGB for OpenGL texture
levels = (-0.08, 0.08)
tex1 = pg.makeRGBA(data[shape[0]/2], levels=levels)[0] # yz plane
tex2 = pg.makeRGBA(data[:,shape[1]/2], levels=levels)[0] # xz plane
tex3 = pg.makeRGBA(data[:,:,shape[2]/2], levels=levels)[0] # xy plane
## Create three image items from textures, add to view
v1 = gl.GLImageItem(tex1)
v1.translate(-shape[1]/2, -shape[2]/2, 0)
v1.rotate(90, 0,0,1)
v1.rotate(-90, 0,1,0)
w.addItem(v1)
v2 = gl.GLImageItem(tex2)
v2.translate(-shape[0]/2, -shape[2]/2, 0)
v2.rotate(-90, 1,0,0)
w.addItem(v2)
v3 = gl.GLImageItem(tex3)
v3.translate(-shape[0]/2, -shape[1]/2, 0)
w.addItem(v3)
ax = gl.GLAxisItem()
w.addItem(ax)
## Start Qt event loop unless running in interactive mode.
if sys.flags.interactive != 1:
app.exec_()

View File

@ -5,6 +5,7 @@ sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', '..'))
from pyqtgraph.Qt import QtCore, QtGui from pyqtgraph.Qt import QtCore, QtGui
import pyqtgraph.opengl as gl import pyqtgraph.opengl as gl
import numpy as np
app = QtGui.QApplication([]) app = QtGui.QApplication([])
w = gl.GLViewWidget() w = gl.GLViewWidget()
@ -14,18 +15,47 @@ w.show()
g = gl.GLGridItem() g = gl.GLGridItem()
w.addItem(g) w.addItem(g)
pts = [ #pos = np.empty((53, 3))
{'pos': (1,0,0), 'size':0.5, 'color':(1.0, 0.0, 0.0, 0.5)}, #size = np.empty((53))
{'pos': (0,1,0), 'size':0.2, 'color':(0.0, 0.0, 1.0, 0.5)}, #color = np.empty((53, 4))
{'pos': (0,0,1), 'size':2./3., 'color':(0.0, 1.0, 0.0, 0.5)}, #pos[0] = (1,0,0); size[0] = 0.5; color[0] = (1.0, 0.0, 0.0, 0.5)
] #pos[1] = (0,1,0); size[1] = 0.2; color[1] = (0.0, 0.0, 1.0, 0.5)
z = 0.5 #pos[2] = (0,0,1); size[2] = 2./3.; color[2] = (0.0, 1.0, 0.0, 0.5)
d = 6.0
for i in range(50): #z = 0.5
pts.append({'pos': (0,0,z), 'size':2./d, 'color':(0.0, 1.0, 0.0, 0.5)}) #d = 6.0
z *= 0.5 #for i in range(3,53):
d *= 2.0 #pos[i] = (0,0,z)
sp = gl.GLScatterPlotItem(pts) #size[i] = 2./d
#color[i] = (0.0, 1.0, 0.0, 0.5)
#z *= 0.5
#d *= 2.0
#sp = gl.GLScatterPlotItem(pos=pos, sizes=size, colors=color, pxMode=False)
pos = (np.random.random(size=(100000,3)) * 10) - 5
color = np.ones((pos.shape[0], 4))
d = (pos**2).sum(axis=1)**0.5
color[:,3] = np.clip(-np.cos(d*2) * 0.2, 0, 1)
sp = gl.GLScatterPlotItem(pos=pos, color=color, size=5)
phase = 0.
def update():
global phase, color, sp, d
s = -np.cos(d*2+phase)
color[:,3] = np.clip(s * 0.2, 0, 1)
color[:,0] = np.clip(s * 3.0, 0, 1)
color[:,1] = np.clip(s * 1.0, 0, 1)
color[:,2] = np.clip(s ** 3, 0, 1)
sp.setData(color=color)
phase -= 0.1
t = QtCore.QTimer()
t.timeout.connect(update)
t.start(50)
w.addItem(sp) w.addItem(sp)
## Start Qt event loop unless running in interactive mode. ## Start Qt event loop unless running in interactive mode.

21
examples/Legend.py Normal file
View File

@ -0,0 +1,21 @@
# -*- coding: utf-8 -*-
import initExample ## Add path to library (just for examples; you do not need this)
import pyqtgraph as pg
from pyqtgraph.Qt import QtCore, QtGui
plt = pg.plot()
l = pg.LegendItem((100,60), (60,10)) # args are (size, position)
l.setParentItem(plt.graphicsItem()) # Note we do NOT call plt.addItem in this case
c1 = plt.plot([1,3,2,4], pen='r')
c2 = plt.plot([2,1,4,3], pen='g')
l.addItem(c1, 'red plot')
l.addItem(c2, 'green plot')
## Start Qt event loop unless running in interactive mode or using pyside.
import sys
if (sys.flags.interactive != 1) or not hasattr(QtCore, 'PYQT_VERSION'):
QtGui.QApplication.instance().exec_()

View File

@ -26,7 +26,7 @@ win.show()
p = ui.plot p = ui.plot
data = np.random.normal(size=(50,500), scale=100) data = np.random.normal(size=(50,500), scale=100)
sizeArray = np.random.random(500) * 20. sizeArray = (np.random.random(500) * 20.).astype(int)
ptr = 0 ptr = 0
lastTime = time() lastTime = time()
fps = None fps = None
@ -49,7 +49,8 @@ def update():
s = np.clip(dt*3., 0, 1) s = np.clip(dt*3., 0, 1)
fps = fps * (1-s) + (1.0/dt) * s fps = fps * (1-s) + (1.0/dt) * s
p.setTitle('%0.2f fps' % fps) p.setTitle('%0.2f fps' % fps)
app.processEvents() ## force complete redraw for every plot p.repaint()
#app.processEvents() ## force complete redraw for every plot
timer = QtCore.QTimer() timer = QtCore.QTimer()
timer.timeout.connect(update) timer.timeout.connect(update)
timer.start(0) timer.start(0)

View File

@ -17,12 +17,11 @@ examples = OrderedDict([
('ImageView', 'ImageView.py'), ('ImageView', 'ImageView.py'),
('ParameterTree', 'parametertree.py'), ('ParameterTree', 'parametertree.py'),
('Crosshair / Mouse interaction', 'crosshair.py'), ('Crosshair / Mouse interaction', 'crosshair.py'),
('Video speed test', 'VideoSpeedTest.py'),
('Plot speed test', 'PlotSpeedTest.py'),
('Data Slicing', 'DataSlicing.py'), ('Data Slicing', 'DataSlicing.py'),
('Plot Customization', 'customPlot.py'), ('Plot Customization', 'customPlot.py'),
('Dock widgets', 'dockarea.py'), ('Dock widgets', 'dockarea.py'),
('Console', 'ConsoleWidget.py'), ('Console', 'ConsoleWidget.py'),
('Histograms', 'histogram.py'),
('GraphicsItems', OrderedDict([ ('GraphicsItems', OrderedDict([
('Scatter Plot', 'ScatterPlot.py'), ('Scatter Plot', 'ScatterPlot.py'),
#('PlotItem', 'PlotItem.py'), #('PlotItem', 'PlotItem.py'),
@ -31,14 +30,22 @@ examples = OrderedDict([
('ImageItem - draw', 'Draw.py'), ('ImageItem - draw', 'Draw.py'),
('Region-of-Interest', 'ROIExamples.py'), ('Region-of-Interest', 'ROIExamples.py'),
('GraphicsLayout', 'GraphicsLayout.py'), ('GraphicsLayout', 'GraphicsLayout.py'),
('LegendItem', 'Legend.py'),
('Text Item', 'text.py'), ('Text Item', 'text.py'),
('Linked Views', 'linkedViews.py'), ('Linked Views', 'linkedViews.py'),
('Arrow', 'Arrow.py'), ('Arrow', 'Arrow.py'),
('ViewBox', 'ViewBox.py'), ('ViewBox', 'ViewBox.py'),
])), ])),
('Benchmarks', OrderedDict([
('Video speed test', 'VideoSpeedTest.py'),
('Line Plot update', 'PlotSpeedTest.py'),
('Scatter Plot update', 'ScatterPlotSpeedTest.py'),
])),
('3D Graphics', OrderedDict([ ('3D Graphics', OrderedDict([
('Volumetric', 'GLVolumeItem.py'), ('Volumetric', 'GLVolumeItem.py'),
('Isosurface', 'GLMeshItem.py'), ('Isosurface', 'GLMeshItem.py'),
('Image', 'GLImageItem.py'),
('Scatter Plot', 'GLScatterPlotItem.py'),
])), ])),
('Widgets', OrderedDict([ ('Widgets', OrderedDict([
('PlotWidget', 'PlotWidget.py'), ('PlotWidget', 'PlotWidget.py'),
@ -80,7 +87,17 @@ class ExampleLoader(QtGui.QMainWindow):
self.ui.loadBtn.clicked.connect(self.loadFile) self.ui.loadBtn.clicked.connect(self.loadFile)
self.ui.exampleTree.currentItemChanged.connect(self.showFile) self.ui.exampleTree.currentItemChanged.connect(self.showFile)
self.ui.exampleTree.itemDoubleClicked.connect(self.loadFile) self.ui.exampleTree.itemDoubleClicked.connect(self.loadFile)
self.ui.pyqtCheck.toggled.connect(self.pyqtToggled)
self.ui.pysideCheck.toggled.connect(self.pysideToggled)
def pyqtToggled(self, b):
if b:
self.ui.pysideCheck.setChecked(False)
def pysideToggled(self, b):
if b:
self.ui.pyqtCheck.setChecked(False)
def populateTree(self, root, examples): def populateTree(self, root, examples):
for key, val in examples.items(): for key, val in examples.items():
@ -101,12 +118,19 @@ class ExampleLoader(QtGui.QMainWindow):
def loadFile(self): def loadFile(self):
fn = self.currentFile() fn = self.currentFile()
extra = []
if self.ui.pyqtCheck.isChecked():
extra.append('pyqt')
elif self.ui.pysideCheck.isChecked():
extra.append('pyside')
if fn is None: if fn is None:
return return
if sys.platform.startswith('win'): if sys.platform.startswith('win'):
os.spawnl(os.P_NOWAIT, sys.executable, sys.executable, '"' + fn + '"') os.spawnl(os.P_NOWAIT, sys.executable, sys.executable, '"' + fn + '"', *extra)
else: else:
os.spawnl(os.P_NOWAIT, sys.executable, sys.executable, fn)
os.spawnl(os.P_NOWAIT, sys.executable, sys.executable, fn, *extra)
def showFile(self): def showFile(self):

View File

@ -39,6 +39,24 @@
</column> </column>
</widget> </widget>
</item> </item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<widget class="QCheckBox" name="pyqtCheck">
<property name="text">
<string>Force PyQt</string>
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="pysideCheck">
<property name="text">
<string>Force PySide</string>
</property>
</widget>
</item>
</layout>
</item>
<item> <item>
<widget class="QPushButton" name="loadBtn"> <widget class="QPushButton" name="loadBtn">
<property name="text"> <property name="text">

View File

@ -2,7 +2,7 @@
# Form implementation generated from reading ui file './examples/exampleLoaderTemplate.ui' # Form implementation generated from reading ui file './examples/exampleLoaderTemplate.ui'
# #
# Created: Sun Sep 9 14:41:31 2012 # Created: Fri Oct 26 07:53:55 2012
# by: PyQt4 UI code generator 4.9.1 # by: PyQt4 UI code generator 4.9.1
# #
# WARNING! All changes made in this file will be lost! # WARNING! All changes made in this file will be lost!
@ -35,6 +35,15 @@ class Ui_Form(object):
self.exampleTree.headerItem().setText(0, _fromUtf8("1")) self.exampleTree.headerItem().setText(0, _fromUtf8("1"))
self.exampleTree.header().setVisible(False) self.exampleTree.header().setVisible(False)
self.verticalLayout.addWidget(self.exampleTree) self.verticalLayout.addWidget(self.exampleTree)
self.horizontalLayout = QtGui.QHBoxLayout()
self.horizontalLayout.setObjectName(_fromUtf8("horizontalLayout"))
self.pyqtCheck = QtGui.QCheckBox(self.layoutWidget)
self.pyqtCheck.setObjectName(_fromUtf8("pyqtCheck"))
self.horizontalLayout.addWidget(self.pyqtCheck)
self.pysideCheck = QtGui.QCheckBox(self.layoutWidget)
self.pysideCheck.setObjectName(_fromUtf8("pysideCheck"))
self.horizontalLayout.addWidget(self.pysideCheck)
self.verticalLayout.addLayout(self.horizontalLayout)
self.loadBtn = QtGui.QPushButton(self.layoutWidget) self.loadBtn = QtGui.QPushButton(self.layoutWidget)
self.loadBtn.setObjectName(_fromUtf8("loadBtn")) self.loadBtn.setObjectName(_fromUtf8("loadBtn"))
self.verticalLayout.addWidget(self.loadBtn) self.verticalLayout.addWidget(self.loadBtn)
@ -51,5 +60,7 @@ class Ui_Form(object):
def retranslateUi(self, Form): def retranslateUi(self, Form):
Form.setWindowTitle(QtGui.QApplication.translate("Form", "Form", None, QtGui.QApplication.UnicodeUTF8)) Form.setWindowTitle(QtGui.QApplication.translate("Form", "Form", None, QtGui.QApplication.UnicodeUTF8))
self.pyqtCheck.setText(QtGui.QApplication.translate("Form", "Force PyQt", None, QtGui.QApplication.UnicodeUTF8))
self.pysideCheck.setText(QtGui.QApplication.translate("Form", "Force PySide", None, QtGui.QApplication.UnicodeUTF8))
self.loadBtn.setText(QtGui.QApplication.translate("Form", "Load Example", None, QtGui.QApplication.UnicodeUTF8)) self.loadBtn.setText(QtGui.QApplication.translate("Form", "Load Example", None, QtGui.QApplication.UnicodeUTF8))

View File

@ -2,7 +2,7 @@
# Form implementation generated from reading ui file './examples/exampleLoaderTemplate.ui' # Form implementation generated from reading ui file './examples/exampleLoaderTemplate.ui'
# #
# Created: Sun Sep 9 14:41:31 2012 # Created: Fri Oct 26 07:53:57 2012
# by: pyside-uic 0.2.13 running on PySide 1.1.0 # by: pyside-uic 0.2.13 running on PySide 1.1.0
# #
# WARNING! All changes made in this file will be lost! # WARNING! All changes made in this file will be lost!
@ -30,6 +30,15 @@ class Ui_Form(object):
self.exampleTree.headerItem().setText(0, "1") self.exampleTree.headerItem().setText(0, "1")
self.exampleTree.header().setVisible(False) self.exampleTree.header().setVisible(False)
self.verticalLayout.addWidget(self.exampleTree) self.verticalLayout.addWidget(self.exampleTree)
self.horizontalLayout = QtGui.QHBoxLayout()
self.horizontalLayout.setObjectName("horizontalLayout")
self.pyqtCheck = QtGui.QCheckBox(self.layoutWidget)
self.pyqtCheck.setObjectName("pyqtCheck")
self.horizontalLayout.addWidget(self.pyqtCheck)
self.pysideCheck = QtGui.QCheckBox(self.layoutWidget)
self.pysideCheck.setObjectName("pysideCheck")
self.horizontalLayout.addWidget(self.pysideCheck)
self.verticalLayout.addLayout(self.horizontalLayout)
self.loadBtn = QtGui.QPushButton(self.layoutWidget) self.loadBtn = QtGui.QPushButton(self.layoutWidget)
self.loadBtn.setObjectName("loadBtn") self.loadBtn.setObjectName("loadBtn")
self.verticalLayout.addWidget(self.loadBtn) self.verticalLayout.addWidget(self.loadBtn)
@ -46,5 +55,7 @@ class Ui_Form(object):
def retranslateUi(self, Form): def retranslateUi(self, Form):
Form.setWindowTitle(QtGui.QApplication.translate("Form", "Form", None, QtGui.QApplication.UnicodeUTF8)) Form.setWindowTitle(QtGui.QApplication.translate("Form", "Form", None, QtGui.QApplication.UnicodeUTF8))
self.pyqtCheck.setText(QtGui.QApplication.translate("Form", "Force PyQt", None, QtGui.QApplication.UnicodeUTF8))
self.pysideCheck.setText(QtGui.QApplication.translate("Form", "Force PySide", None, QtGui.QApplication.UnicodeUTF8))
self.loadBtn.setText(QtGui.QApplication.translate("Form", "Load Example", None, QtGui.QApplication.UnicodeUTF8)) self.loadBtn.setText(QtGui.QApplication.translate("Form", "Load Example", None, QtGui.QApplication.UnicodeUTF8))

38
examples/histogram.py Normal file
View File

@ -0,0 +1,38 @@
# -*- coding: utf-8 -*-
"""
In this example we draw two different kinds of histogram.
"""
import initExample ## Add path to library (just for examples; you do not need this)
import pyqtgraph as pg
from pyqtgraph.Qt import QtCore, QtGui
import numpy as np
win = pg.GraphicsWindow()
win.resize(800,350)
plt1 = win.addPlot()
plt2 = win.addPlot()
## make interesting distribution of values
vals = np.hstack([np.random.normal(size=500), np.random.normal(size=260, loc=4)])
## draw standard histogram
y,x = np.histogram(vals, bins=np.linspace(-3, 8, 40))
## notice that len(x) == len(y)+1
## We are required to use stepMode=True so that PlotCurveItem will interpret this data correctly.
curve = pg.PlotCurveItem(x, y, stepMode=True, fillLevel=0, brush=(0, 0, 255, 80))
plt1.addItem(curve)
## Now draw all points as a nicely-spaced scatter plot
y = pg.pseudoScatter(vals, spacing=0.15)
#plt2.plot(vals, y, pen=None, symbol='o', symbolSize=5)
plt2.plot(vals, y, pen=None, symbol='o', symbolSize=5, symbolPen=(255,255,255,200), symbolBrush=(0,0,255,150))
## Start Qt event loop unless running in interactive mode or using pyside.
import sys
if (sys.flags.interactive != 1) or not hasattr(QtCore, 'PYQT_VERSION'):
QtGui.QApplication.instance().exec_()

View File

@ -1,3 +1,8 @@
## make this version of pyqtgraph importable before any others ## make this version of pyqtgraph importable before any others
import sys, os import sys, os
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..'))) sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..')))
if 'pyside' in sys.argv: ## should force example to use PySide instead of PyQt
import PySide
elif 'pyqt' in sys.argv:
import PyQt4

View File

@ -22,7 +22,7 @@ class CSVExporter(Exporter):
def export(self, fileName=None): def export(self, fileName=None):
if not isinstance(self.item, pg.PlotItem): if not isinstance(self.item, pg.PlotItem):
raise Exception("Matplotlib export currently only works with plot items") raise Exception("Must have a PlotItem selected for CSV export.")
if fileName is None: if fileName is None:
self.fileSaveDialog(filter=["*.csv", "*.tsv"]) self.fileSaveDialog(filter=["*.csv", "*.tsv"])

View File

@ -54,8 +54,13 @@ class Exporter(object):
fileName = str(fileName) fileName = str(fileName)
global LastExportDirectory global LastExportDirectory
LastExportDirectory = os.path.split(fileName)[0] LastExportDirectory = os.path.split(fileName)[0]
self.export(fileName=fileName, **self.fileDialog.opts)
ext = os.path.splitext(fileName)[1].lower()
selectedExt = str(self.fileDialog.selectedNameFilter()).lstrip('*').lower()
if ext != selectedExt:
fileName = fileName + selectedExt
self.export(fileName=fileName, **self.fileDialog.opts)
def getScene(self): def getScene(self):
if isinstance(self.item, pg.GraphicsScene): if isinstance(self.item, pg.GraphicsScene):

View File

@ -63,7 +63,7 @@ class ImageExporter(Exporter):
self.getScene().render(painter, QtCore.QRectF(targetRect), sourceRect) self.getScene().render(painter, QtCore.QRectF(targetRect), sourceRect)
finally: finally:
self.setExportMode(False) self.setExportMode(False)
self.png.save(fileName)
painter.end() painter.end()
self.png.save(fileName)

View File

@ -22,9 +22,10 @@ SI_PREFIXES_ASCII = 'yzafpnum kMGTPEZY'
from .Qt import QtGui, QtCore from .Qt import QtGui, QtCore, USE_PYSIDE
import numpy as np import numpy as np
import decimal, re import decimal, re
import ctypes
try: try:
import scipy.ndimage import scipy.ndimage
@ -223,13 +224,15 @@ def mkColor(*args):
return QtGui.QColor(*args) return QtGui.QColor(*args)
def mkBrush(*args): def mkBrush(*args, **kwds):
""" """
| Convenience function for constructing Brush. | Convenience function for constructing Brush.
| This function always constructs a solid brush and accepts the same arguments as :func:`mkColor() <pyqtgraph.mkColor>` | This function always constructs a solid brush and accepts the same arguments as :func:`mkColor() <pyqtgraph.mkColor>`
| Calling mkBrush(None) returns an invisible brush. | Calling mkBrush(None) returns an invisible brush.
""" """
if len(args) == 1: if 'color' in kwds:
color = kwds['color']
elif len(args) == 1:
arg = args[0] arg = args[0]
if arg is None: if arg is None:
return QtGui.QBrush(QtCore.Qt.NoBrush) return QtGui.QBrush(QtCore.Qt.NoBrush)
@ -237,7 +240,7 @@ def mkBrush(*args):
return QtGui.QBrush(arg) return QtGui.QBrush(arg)
else: else:
color = arg color = arg
if len(args) > 1: elif len(args) > 1:
color = args color = args
return QtGui.QBrush(mkColor(color)) return QtGui.QBrush(mkColor(color))
@ -579,7 +582,10 @@ def solveBilinearTransform(points1, points2):
def makeRGBA(*args, **kwds):
"""Equivalent to makeARGB(..., useRGBA=True)"""
kwds['useRGBA'] = True
return makeARGB(*args, **kwds)
def makeARGB(data, lut=None, levels=None, useRGBA=False): def makeARGB(data, lut=None, levels=None, useRGBA=False):
""" """
@ -605,7 +611,7 @@ def makeARGB(data, lut=None, levels=None, useRGBA=False):
Lookup tables can be built using GradientWidget. Lookup tables can be built using GradientWidget.
levels - List [min, max]; optionally rescale data before converting through the levels - List [min, max]; optionally rescale data before converting through the
lookup table. rescaled = (data-min) * len(lut) / (max-min) lookup table. rescaled = (data-min) * len(lut) / (max-min)
useRGBA - If True, the data is returned in RGBA order. The default is useRGBA - If True, the data is returned in RGBA order (useful for building OpenGL textures). The default is
False, which returns in BGRA order for use with QImage. False, which returns in BGRA order for use with QImage.
""" """
@ -779,30 +785,117 @@ def makeARGB(data, lut=None, levels=None, useRGBA=False):
return imgData, alpha return imgData, alpha
def makeQImage(imgData, alpha): def makeQImage(imgData, alpha=None, copy=True, transpose=True):
"""Turn an ARGB array into QImage""" """
Turn an ARGB array into QImage.
By default, the data is copied; changes to the array will not
be reflected in the image. The image will be given a 'data' attribute
pointing to the array which shares its data to prevent python
freeing that memory while the image is in use.
=========== ===================================================================
Arguments:
imgData Array of data to convert. Must have shape (width, height, 3 or 4)
and dtype=ubyte. The order of values in the 3rd axis must be
(b, g, r, a).
alpha If True, the QImage returned will have format ARGB32. If False,
the format will be RGB32. By default, _alpha_ is True if
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.
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
opposite.
=========== ===================================================================
"""
## create QImage from buffer ## create QImage from buffer
prof = debug.Profiler('functions.makeQImage', disabled=True) prof = debug.Profiler('functions.makeQImage', disabled=True)
## If we didn't explicitly specify alpha, check the array shape.
if alpha is None:
alpha = (imgData.shape[2] == 4)
copied = False
if imgData.shape[2] == 3: ## need to make alpha channel (even if alpha==False; QImage requires 32 bpp)
if copy is True:
d2 = np.empty(imgData.shape[:2] + (4,), dtype=imgData.dtype)
d2[:,:,:3] = imgData
d2[:,:,3] = 255
imgData = d2
copied = True
else:
raise Exception('Array has only 3 channels; cannot make QImage without copying.')
if alpha: if alpha:
imgFormat = QtGui.QImage.Format_ARGB32 imgFormat = QtGui.QImage.Format_ARGB32
else: else:
imgFormat = QtGui.QImage.Format_RGB32 imgFormat = QtGui.QImage.Format_RGB32
imgData = imgData.transpose((1, 0, 2)) ## QImage expects the row/column order to be opposite if transpose:
try: imgData = imgData.transpose((1, 0, 2)) ## QImage expects the row/column order to be opposite
buf = imgData.data
except AttributeError: ## happens when image data is non-contiguous if not imgData.flags['C_CONTIGUOUS']:
if copy is False:
extra = ' (try setting transpose=False)' if transpose else ''
raise Exception('Array is not contiguous; cannot make QImage without copying.'+extra)
imgData = np.ascontiguousarray(imgData) imgData = np.ascontiguousarray(imgData)
buf = imgData.data copied = True
prof.mark('1') if copy is True and copied is False:
qimage = QtGui.QImage(buf, imgData.shape[1], imgData.shape[0], imgFormat) imgData = imgData.copy()
prof.mark('2')
qimage.data = imgData if USE_PYSIDE:
prof.finish() ch = ctypes.c_char.from_buffer(imgData, 0)
return qimage img = QtGui.QImage(ch, imgData.shape[1], imgData.shape[0], imgFormat)
else:
addr = ctypes.addressof(ctypes.c_char.from_buffer(imgData, 0))
img = QtGui.QImage(addr, imgData.shape[1], imgData.shape[0], imgFormat)
img.data = imgData
return img
#try:
#buf = imgData.data
#except AttributeError: ## happens when image data is non-contiguous
#buf = imgData.data
#prof.mark('1')
#qimage = QtGui.QImage(buf, imgData.shape[1], imgData.shape[0], imgFormat)
#prof.mark('2')
#qimage.data = imgData
#prof.finish()
#return qimage
def imageToArray(img, copy=False, transpose=True):
"""
Convert a QImage into numpy array. The image must have format RGB32, ARGB32, or ARGB32_Premultiplied.
By default, the image is not copied; changes made to the array will appear in the QImage as well (beware: if
the QImage is collected before the array, there may be trouble).
The array will have shape (width, height, (b,g,r,a)).
"""
fmt = img.format()
ptr = img.bits()
if USE_PYSIDE:
arr = np.frombuffer(ptr, dtype=np.ubyte)
else:
ptr.setsize(img.byteCount())
arr = np.asarray(ptr)
if fmt == img.Format_RGB32:
arr = arr.reshape(img.height(), img.width(), 3)
elif fmt == img.Format_ARGB32 or fmt == img.Format_ARGB32_Premultiplied:
arr = arr.reshape(img.height(), img.width(), 4)
if copy:
arr = arr.copy()
if transpose:
return arr.transpose((1,0,2))
else:
return arr
def rescaleData(data, scale, offset): def rescaleData(data, scale, offset):
newData = np.empty((data.size,), dtype=np.int) newData = np.empty((data.size,), dtype=np.int)
@ -1386,3 +1479,52 @@ 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]) 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):
"""
Used for examining the distribution of values in a set.
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).
"""
inds = np.arange(len(data))
if shuffle:
np.random.shuffle(inds)
data = data[inds]
if spacing is None:
spacing = 2.*np.std(data)/len(data)**0.5
s2 = spacing**2
yvals = np.empty(len(data))
yvals[0] = 0
for i in range(1,len(data)):
x = data[i] # current x value to be placed
x0 = data[:i] # all x values already placed
y0 = yvals[:i] # all y values already placed
y = 0
dx = (x0-x)**2 # x-distance to each previous point
xmask = dx < s2 # exclude anything too far away
if xmask.sum() > 0:
dx = dx[xmask]
dy = (s2 - dx)**0.5
limits = np.empty((2,len(dy))) # ranges of y-values to exclude
limits[0] = y0[xmask] - dy
limits[1] = y0[xmask] + dy
while True:
# ignore anything below this y-value
mask = limits[1] >= y
limits = limits[:,mask]
# are we inside an excluded region?
mask = (limits[0] < y) & (limits[1] > y)
if mask.sum() == 0:
break
y = limits[:,mask].max()
yvals[i] = y
return yvals[np.argsort(inds)] ## un-shuffle values before returning

View File

@ -172,6 +172,8 @@ class AxisItem(GraphicsWidget):
return asUnicode("<span style='%s'>%s</span>") % (style, s) return asUnicode("<span style='%s'>%s</span>") % (style, s)
def setHeight(self, h=None): 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: if h is None:
h = self.textHeight + max(0, self.tickLength) h = self.textHeight + max(0, self.tickLength)
if self.label.isVisible(): if self.label.isVisible():
@ -182,6 +184,8 @@ class AxisItem(GraphicsWidget):
def setWidth(self, w=None): def setWidth(self, w=None):
"""Set the width of this axis reserved for ticks and tick labels.
The width of the axis label is automatically added."""
if w is None: if w is None:
w = max(0, self.tickLength) + 40 w = max(0, self.tickLength) + 40
if self.label.isVisible(): if self.label.isVisible():

View File

@ -394,14 +394,17 @@ class GraphicsItem(object):
if oldView is not None: if oldView is not None:
#print "disconnect:", self, oldView #print "disconnect:", self, oldView
oldView.sigRangeChanged.disconnect(self.viewRangeChanged) oldView.sigRangeChanged.disconnect(self.viewRangeChanged)
oldView.sigTransformChanged.disconnect(self.viewTransformChanged)
self._connectedView = None self._connectedView = None
## connect to new view ## connect to new view
if view is not None: if view is not None:
#print "connect:", self, view #print "connect:", self, view
view.sigRangeChanged.connect(self.viewRangeChanged) view.sigRangeChanged.connect(self.viewRangeChanged)
view.sigTransformChanged.connect(self.viewTransformChanged)
self._connectedView = weakref.ref(view) self._connectedView = weakref.ref(view)
self.viewRangeChanged() self.viewRangeChanged()
self.viewTransformChanged()
## inform children that their view might have changed ## inform children that their view might have changed
self._replaceView(oldView) self._replaceView(oldView)
@ -425,3 +428,9 @@ class GraphicsItem(object):
Called whenever the view coordinates of the ViewBox containing this item have changed. Called whenever the view coordinates of the ViewBox containing this item have changed.
""" """
pass pass
def viewTransformChanged(self):
"""
Called whenever the transformation matrix of the view has changed.
"""
pass

View File

@ -0,0 +1,68 @@
from .GraphicsWidget import GraphicsWidget
from .LabelItem import LabelItem
from ..Qt import QtGui, QtCore
from .. import functions as fn
__all__ = ['LegendItem']
class LegendItem(GraphicsWidget):
"""
Displays a legend used for describing the contents of a plot.
Note that this item should not be added directly to a PlotItem. Instead,
Make it a direct descendant of the PlotItem::
legend.setParentItem(plotItem)
"""
def __init__(self, size, offset):
GraphicsWidget.__init__(self)
self.setFlag(self.ItemIgnoresTransformations)
self.layout = QtGui.QGraphicsGridLayout()
self.setLayout(self.layout)
self.items = []
self.size = size
self.offset = offset
self.setGeometry(QtCore.QRectF(self.offset[0], self.offset[1], self.size[0], self.size[1]))
def addItem(self, item, title):
"""
Add a new entry to the legend.
=========== ========================================================
Arguments
item A PlotDataItem from which the line and point style
of the item will be determined
title The title to display for this item. Simple HTML allowed.
=========== ========================================================
"""
label = LabelItem(title)
sample = ItemSample(item)
row = len(self.items)
self.items.append((sample, label))
self.layout.addItem(sample, row, 0)
self.layout.addItem(label, row, 1)
def boundingRect(self):
return QtCore.QRectF(0, 0, self.size[0], self.size[1])
def paint(self, p, *args):
p.setPen(fn.mkPen(255,255,255,100))
p.setBrush(fn.mkBrush(100,100,100,50))
p.drawRect(self.boundingRect())
class ItemSample(GraphicsWidget):
def __init__(self, item):
GraphicsWidget.__init__(self)
self.item = item
def boundingRect(self):
return QtCore.QRectF(0, 0, 20, 20)
def paint(self, p, *args):
p.setPen(fn.mkPen(self.item.opts['pen']))
p.drawLine(2, 18, 18, 2)

View File

@ -64,6 +64,7 @@ class PlotCurveItem(GraphicsObject):
'shadowPen': None, 'shadowPen': None,
'fillLevel': None, 'fillLevel': None,
'brush': None, 'brush': None,
'stepMode': False,
} }
self.setClickable(kargs.get('clickable', False)) self.setClickable(kargs.get('clickable', False))
self.setData(*args, **kargs) self.setData(*args, **kargs)
@ -223,8 +224,15 @@ class PlotCurveItem(GraphicsObject):
prof.mark('copy') prof.mark('copy')
if self.xData.shape != self.yData.shape: if 'stepMode' in kargs:
raise Exception("X and Y arrays must be the same shape--got %s and %s." % (str(x.shape), str(y.shape))) self.opts['stepMode'] = kargs['stepMode']
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)))
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)))
self.path = None self.path = None
self.fillPath = None self.fillPath = None
@ -267,6 +275,29 @@ class PlotCurveItem(GraphicsObject):
## 0(i4) ## 0(i4)
## ##
## All values are big endian--pack using struct.pack('>d') or struct.pack('>i') ## All values are big endian--pack using struct.pack('>d') or struct.pack('>i')
if self.opts['stepMode']:
## each value in the x/y arrays generates 2 points.
x2 = np.empty((len(x),2), dtype=x.dtype)
x2[:] = x[:,np.newaxis]
if self.opts['fillLevel'] is None:
x = x2.reshape(x2.size)[1:-1]
y2 = np.empty((len(y),2), dtype=y.dtype)
y2[:] = y[:,np.newaxis]
y = y2.reshape(y2.size)
else:
## If we have a fill level, add two extra points at either end
x = x2.reshape(x2.size)
y2 = np.empty((len(y)+2,2), dtype=y.dtype)
y2[1:-1] = y[:,np.newaxis]
y = y2.reshape(y2.size)[1:-1]
y[0] = self.opts['fillLevel']
y[-1] = self.opts['fillLevel']
if sys.version_info[0] == 2: ## So this is disabled for python 3... why?? if sys.version_info[0] == 2: ## So this is disabled for python 3... why??
n = x.shape[0] n = x.shape[0]
# create empty array, pad with extra space on either end # create empty array, pad with extra space on either end
@ -324,12 +355,21 @@ class PlotCurveItem(GraphicsObject):
pixels = self.pixelVectors() pixels = self.pixelVectors()
if pixels == (None, None): if pixels == (None, None):
pixels = [Point(0,0), Point(0,0)] pixels = [Point(0,0), Point(0,0)]
xmin = x.min() - pixels[0].x() * lineWidth
xmax = x.max() + pixels[0].x() * lineWidth
ymin = y.min() - abs(pixels[1].y()) * lineWidth
ymax = y.max() + abs(pixels[1].y()) * lineWidth
xmin = x.min()
xmax = x.max()
ymin = y.min()
ymax = y.max()
if self.opts['fillLevel'] is not None:
ymin = min(ymin, self.opts['fillLevel'])
ymax = max(ymax, self.opts['fillLevel'])
xmin -= pixels[0].x() * lineWidth
xmax += pixels[0].x() * lineWidth
ymin -= abs(pixels[1].y()) * lineWidth
ymax += abs(pixels[1].y()) * lineWidth
return QtCore.QRectF(xmin, ymin, xmax-xmin, ymax-ymin) return QtCore.QRectF(xmin, ymin, xmax-xmin, ymax-ymin)
def paint(self, p, opt, widget): def paint(self, p, opt, widget):

View File

@ -130,6 +130,8 @@ class PlotDataItem(GraphicsObject):
'symbolBrush': (50, 50, 150), 'symbolBrush': (50, 50, 150),
'pxMode': True, 'pxMode': True,
'pointMode': None,
'data': None, 'data': None,
} }
self.setData(*args, **kargs) self.setData(*args, **kargs)
@ -144,22 +146,30 @@ class PlotDataItem(GraphicsObject):
return QtCore.QRectF() ## let child items handle this return QtCore.QRectF() ## let child items handle this
def setAlpha(self, alpha, auto): def setAlpha(self, alpha, auto):
if self.opts['alphaHint'] == alpha and self.opts['alphaMode'] == auto:
return
self.opts['alphaHint'] = alpha self.opts['alphaHint'] = alpha
self.opts['alphaMode'] = auto self.opts['alphaMode'] = auto
self.setOpacity(alpha) self.setOpacity(alpha)
#self.update() #self.update()
def setFftMode(self, mode): def setFftMode(self, mode):
if self.opts['fftMode'] == mode:
return
self.opts['fftMode'] = mode self.opts['fftMode'] = mode
self.xDisp = self.yDisp = None self.xDisp = self.yDisp = None
self.updateItems() self.updateItems()
def setLogMode(self, xMode, yMode): def setLogMode(self, xMode, yMode):
self.opts['logMode'] = (xMode, yMode) if self.opts['logMode'] == [xMode, yMode]:
return
self.opts['logMode'] = [xMode, yMode]
self.xDisp = self.yDisp = None self.xDisp = self.yDisp = None
self.updateItems() self.updateItems()
def setPointMode(self, mode): def setPointMode(self, mode):
if self.opts['pointMode'] == mode:
return
self.opts['pointMode'] = mode self.opts['pointMode'] = mode
self.update() self.update()
@ -193,6 +203,8 @@ class PlotDataItem(GraphicsObject):
def setFillBrush(self, *args, **kargs): def setFillBrush(self, *args, **kargs):
brush = fn.mkBrush(*args, **kargs) brush = fn.mkBrush(*args, **kargs)
if self.opts['fillBrush'] == brush:
return
self.opts['fillBrush'] = brush self.opts['fillBrush'] = brush
self.updateItems() self.updateItems()
@ -200,16 +212,22 @@ class PlotDataItem(GraphicsObject):
return self.setFillBrush(*args, **kargs) return self.setFillBrush(*args, **kargs)
def setFillLevel(self, level): def setFillLevel(self, level):
if self.opts['fillLevel'] == level:
return
self.opts['fillLevel'] = level self.opts['fillLevel'] = level
self.updateItems() self.updateItems()
def setSymbol(self, symbol): def setSymbol(self, symbol):
if self.opts['symbol'] == symbol:
return
self.opts['symbol'] = symbol self.opts['symbol'] = symbol
#self.scatter.setSymbol(symbol) #self.scatter.setSymbol(symbol)
self.updateItems() self.updateItems()
def setSymbolPen(self, *args, **kargs): def setSymbolPen(self, *args, **kargs):
pen = fn.mkPen(*args, **kargs) pen = fn.mkPen(*args, **kargs)
if self.opts['symbolPen'] == pen:
return
self.opts['symbolPen'] = pen self.opts['symbolPen'] = pen
#self.scatter.setSymbolPen(pen) #self.scatter.setSymbolPen(pen)
self.updateItems() self.updateItems()
@ -218,21 +236,26 @@ class PlotDataItem(GraphicsObject):
def setSymbolBrush(self, *args, **kargs): def setSymbolBrush(self, *args, **kargs):
brush = fn.mkBrush(*args, **kargs) brush = fn.mkBrush(*args, **kargs)
if self.opts['symbolBrush'] == brush:
return
self.opts['symbolBrush'] = brush self.opts['symbolBrush'] = brush
#self.scatter.setSymbolBrush(brush) #self.scatter.setSymbolBrush(brush)
self.updateItems() self.updateItems()
def setSymbolSize(self, size): def setSymbolSize(self, size):
if self.opts['symbolSize'] == size:
return
self.opts['symbolSize'] = size self.opts['symbolSize'] = size
#self.scatter.setSymbolSize(symbolSize) #self.scatter.setSymbolSize(symbolSize)
self.updateItems() self.updateItems()
def setDownsampling(self, ds): def setDownsampling(self, ds):
if self.opts['downsample'] != ds: if self.opts['downsample'] == ds:
self.opts['downsample'] = ds return
self.xDisp = self.yDisp = None self.opts['downsample'] = ds
self.updateItems() self.xDisp = self.yDisp = None
self.updateItems()
def setData(self, *args, **kargs): def setData(self, *args, **kargs):
""" """
@ -436,9 +459,12 @@ class PlotDataItem(GraphicsObject):
and max) and max)
=============== ============================================================= =============== =============================================================
""" """
if frac <= 0.0:
raise Exception("Value for parameter 'frac' must be > 0. (got %s)" % str(frac))
(x, y) = self.getData() (x, y) = self.getData()
if x is None or len(x) == 0: if x is None or len(x) == 0:
return (0, 0) return None
if ax == 0: if ax == 0:
d = x d = x
@ -450,14 +476,15 @@ class PlotDataItem(GraphicsObject):
if orthoRange is not None: if orthoRange is not None:
mask = (d2 >= orthoRange[0]) * (d2 <= orthoRange[1]) mask = (d2 >= orthoRange[0]) * (d2 <= orthoRange[1])
d = d[mask] d = d[mask]
d2 = d2[mask] #d2 = d2[mask]
if frac >= 1.0: if len(d) > 0:
return (np.min(d), np.max(d)) if frac >= 1.0:
elif frac <= 0.0: return (np.min(d), np.max(d))
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)))
else: else:
return (scipy.stats.scoreatpercentile(d, 50 - (frac * 50)), scipy.stats.scoreatpercentile(d, 50 + (frac * 50))) return None
def clear(self): def clear(self):

View File

@ -1,4 +1,4 @@
from pyqtgraph.Qt import QtGui, QtCore from pyqtgraph.Qt import QtGui, QtCore, USE_PYSIDE
from pyqtgraph.Point import Point from pyqtgraph.Point import Point
import pyqtgraph.functions as fn import pyqtgraph.functions as fn
from .GraphicsItem import GraphicsItem from .GraphicsItem import GraphicsItem
@ -32,26 +32,171 @@ for k, c in coords.items():
Symbols[k].lineTo(x, y) Symbols[k].lineTo(x, y)
Symbols[k].closeSubpath() Symbols[k].closeSubpath()
def drawSymbol(painter, symbol, size, pen, brush):
painter.scale(size, size)
painter.setPen(pen)
painter.setBrush(brush)
if isinstance(symbol, basestring):
symbol = Symbols[symbol]
if np.isscalar(symbol):
symbol = Symbols.values()[symbol % len(Symbols)]
painter.drawPath(symbol)
def makeSymbolPixmap(size, pen, brush, symbol):
def renderSymbol(symbol, size, pen, brush, device=None):
"""
Render a symbol specification to QImage.
Symbol may be either a QPainterPath or one of the keys in the Symbols dict.
If *device* is None, a new QPixmap will be returned. Otherwise,
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 ## Render a spot with the given parameters to a pixmap
penPxWidth = max(np.ceil(pen.width()), 1) penPxWidth = max(np.ceil(pen.width()), 1)
image = QtGui.QImage(int(size+penPxWidth), int(size+penPxWidth), QtGui.QImage.Format_ARGB32_Premultiplied) image = QtGui.QImage(int(size+penPxWidth), int(size+penPxWidth), QtGui.QImage.Format_ARGB32)
image.fill(0) image.fill(0)
p = QtGui.QPainter(image) p = QtGui.QPainter(image)
p.setRenderHint(p.Antialiasing) p.setRenderHint(p.Antialiasing)
p.translate(image.width()*0.5, image.height()*0.5) p.translate(image.width()*0.5, image.height()*0.5)
p.scale(size, size) drawSymbol(p, symbol, size, pen, brush)
p.setPen(pen)
p.setBrush(brush)
if isinstance(symbol, basestring):
symbol = Symbols[symbol]
p.drawPath(symbol)
p.end() p.end()
return QtGui.QPixmap(image) return image
#pixmap = QtGui.QPixmap(image)
#SymbolPixmapCache[key] = pixmap
#return pixmap
def makeSymbolPixmap(size, pen, brush, symbol):
## deprecated
img = renderSymbol(symbol, size, pen, brush)
return QtGui.QPixmap(img)
class SymbolAtlas:
"""
Used to efficiently construct a single QPixmap containing all rendered symbols
for a ScatterPlotItem. This is required for fragment rendering.
Use example:
atlas = SymbolAtlas()
sc1 = atlas.getSymbolCoords('o', 5, QPen(..), QBrush(..))
sc2 = atlas.getSymbolCoords('t', 10, QPen(..), QBrush(..))
pm = atlas.getAtlas()
"""
class SymbolCoords(list): ## needed because lists are not allowed in weak references.
pass
def __init__(self):
# symbol key : [x, y, w, h] atlas coordinates
# note that the coordinate list will always be the same list object as
# long as the symbol is in the atlas, but the coordinates may
# change if the atlas is rebuilt.
# weak value; if all external refs to this list disappear,
# the symbol will be forgotten.
self.symbolMap = weakref.WeakValueDictionary()
self.atlasData = None # numpy array of atlas image
self.atlas = None # atlas as QPixmap
self.atlasValid = False
def getSymbolCoords(self, opts):
"""
Given a list of spot records, return an object representing the coordinates of that symbol within the atlas
"""
coords = np.empty(len(opts), dtype=object)
for i, rec in enumerate(opts):
symbol, size, pen, brush = rec['symbol'], rec['size'], rec['pen'], rec['brush']
pen = fn.mkPen(pen) if not isinstance(pen, QtGui.QPen) else pen
brush = fn.mkBrush(brush) if not isinstance(pen, QtGui.QBrush) else brush
key = (symbol, size, fn.colorTuple(pen.color()), pen.width(), pen.style(), fn.colorTuple(brush.color()))
if key not in self.symbolMap:
newCoords = SymbolAtlas.SymbolCoords()
self.symbolMap[key] = newCoords
self.atlasValid = False
#try:
#self.addToAtlas(key) ## squeeze this into the atlas if there is room
#except:
#self.buildAtlas() ## otherwise, we need to rebuild
coords[i] = self.symbolMap[key]
return coords
def buildAtlas(self):
# get rendered array for all symbols, keep track of avg/max width
rendered = {}
avgWidth = 0.0
maxWidth = 0
images = []
for key, coords in self.symbolMap.items():
if len(coords) == 0:
pen = fn.mkPen(color=key[2], width=key[3], style=key[4])
brush = fn.mkBrush(color=key[5])
img = renderSymbol(key[0], key[1], pen, brush)
images.append(img) ## we only need this to prevent the images being garbage collected immediately
arr = fn.imageToArray(img, copy=False, transpose=False)
else:
(x,y,w,h) = self.symbolMap[key]
arr = self.atlasData[x:x+w, y:y+w]
rendered[key] = arr
w = arr.shape[0]
avgWidth += w
maxWidth = max(maxWidth, w)
nSymbols = len(rendered)
if nSymbols > 0:
avgWidth /= nSymbols
width = max(maxWidth, avgWidth * (nSymbols**0.5))
else:
avgWidth = 0
width = 0
# sort symbols by height
symbols = sorted(rendered.keys(), key=lambda x: rendered[x].shape[1], reverse=True)
self.atlasRows = []
x = width
y = 0
rowheight = 0
for key in symbols:
arr = rendered[key]
w,h = arr.shape[:2]
if x+w > width:
y += rowheight
x = 0
rowheight = h
self.atlasRows.append([y, rowheight, 0])
self.symbolMap[key][:] = x, y, w, h
x += w
self.atlasRows[-1][2] = x
height = y + rowheight
self.atlasData = np.zeros((width, height, 4), dtype=np.ubyte)
for key in symbols:
x, y, w, h = self.symbolMap[key]
self.atlasData[x:x+w, y:y+h] = rendered[key]
self.atlas = None
self.atlasValid = True
def getAtlas(self):
if not self.atlasValid:
self.buildAtlas()
if self.atlas is None:
if len(self.atlasData) == 0:
return QtGui.QPixmap(0,0)
img = fn.makeQImage(self.atlasData, copy=False, transpose=False)
self.atlas = QtGui.QPixmap(img)
return self.atlas
class ScatterPlotItem(GraphicsObject): class ScatterPlotItem(GraphicsObject):
""" """
Displays a set of x/y points. Instances of this class are created Displays a set of x/y points. Instances of this class are created
@ -79,13 +224,16 @@ class ScatterPlotItem(GraphicsObject):
""" """
prof = debug.Profiler('ScatterPlotItem.__init__', disabled=True) prof = debug.Profiler('ScatterPlotItem.__init__', disabled=True)
GraphicsObject.__init__(self) GraphicsObject.__init__(self)
self.setFlag(self.ItemHasNoContents, True)
self.data = np.empty(0, dtype=[('x', float), ('y', float), ('size', float), ('symbol', object), ('pen', object), ('brush', object), ('item', object), ('data', object)]) self.picture = None # QPicture used for rendering when pxmode==False
self.fragments = None # fragment specification for pxmode; updated every time the view changes.
self.fragmentAtlas = SymbolAtlas()
self.data = np.empty(0, dtype=[('x', float), ('y', float), ('size', float), ('symbol', object), ('pen', object), ('brush', object), ('data', object), ('fragCoords', object), ('item', object)])
self.bounds = [None, None] ## caches data bounds self.bounds = [None, None] ## caches data bounds
self._maxSpotWidth = 0 ## maximum size of the scale-variant portion of all spots self._maxSpotWidth = 0 ## maximum size of the scale-variant portion of all spots
self._maxSpotPxWidth = 0 ## maximum size of the scale-invariant portion of all spots self._maxSpotPxWidth = 0 ## maximum size of the scale-invariant portion of all spots
self._spotPixmap = None self.opts = {'pxMode': True, 'useCache': True} ## If useCache is False, symbols are re-drawn on every paint.
self.opts = {'pxMode': True}
self.setPen(200,200,200, update=False) self.setPen(200,200,200, update=False)
self.setBrush(100,100,150, update=False) self.setBrush(100,100,150, update=False)
@ -96,6 +244,8 @@ class ScatterPlotItem(GraphicsObject):
prof.mark('setData') prof.mark('setData')
prof.finish() prof.finish()
#self.setCacheMode(self.DeviceCoordinateCache)
def setData(self, *args, **kargs): def setData(self, *args, **kargs):
""" """
**Ordered Arguments:** **Ordered Arguments:**
@ -130,6 +280,7 @@ class ScatterPlotItem(GraphicsObject):
*identical* *Deprecated*. This functionality is handled automatically now. *identical* *Deprecated*. This functionality is handled automatically now.
====================== =============================================================================================== ====================== ===============================================================================================
""" """
oldData = self.data ## this causes cached pixmaps to be preserved while new data is registered.
self.clear() ## clear out all old data self.clear() ## clear out all old data
self.addPoints(*args, **kargs) self.addPoints(*args, **kargs)
@ -183,8 +334,8 @@ class ScatterPlotItem(GraphicsObject):
## note that np.empty initializes object fields to None and string fields to '' ## note that np.empty initializes object fields to None and string fields to ''
self.data[:len(oldData)] = oldData self.data[:len(oldData)] = oldData
for i in range(len(oldData)): #for i in range(len(oldData)):
oldData[i]['item']._data = self.data[i] ## Make sure items have proper reference to new array #oldData[i]['item']._data = self.data[i] ## Make sure items have proper reference to new array
newData = self.data[len(oldData):] newData = self.data[len(oldData):]
newData['size'] = -1 ## indicates to use default size newData['size'] = -1 ## indicates to use default size
@ -217,7 +368,7 @@ class ScatterPlotItem(GraphicsObject):
newData['y'] = kargs['y'] newData['y'] = kargs['y']
if 'pxMode' in kargs: if 'pxMode' in kargs:
self.setPxMode(kargs['pxMode'], update=False) self.setPxMode(kargs['pxMode'])
## Set any extra parameters provided in keyword arguments ## Set any extra parameters provided in keyword arguments
for k in ['pen', 'brush', 'symbol', 'size']: for k in ['pen', 'brush', 'symbol', 'size']:
@ -228,12 +379,18 @@ class ScatterPlotItem(GraphicsObject):
if 'data' in kargs: if 'data' in kargs:
self.setPointData(kargs['data'], dataSet=newData) self.setPointData(kargs['data'], dataSet=newData)
#self.updateSpots()
self.prepareGeometryChange() self.prepareGeometryChange()
self.bounds = [None, None] self.bounds = [None, None]
self.generateSpotItems() self.invalidate()
self.updateSpots(newData)
self.sigPlotChanged.emit(self) self.sigPlotChanged.emit(self)
def invalidate(self):
## clear any cached drawing state
self.picture = None
self.fragments = None
self.update()
def getData(self): def getData(self):
return self.data['x'], self.data['y'] return self.data['x'], self.data['y']
@ -263,8 +420,8 @@ class ScatterPlotItem(GraphicsObject):
dataSet['pen'] = pens dataSet['pen'] = pens
else: else:
self.opts['pen'] = fn.mkPen(*args, **kargs) self.opts['pen'] = fn.mkPen(*args, **kargs)
self._spotPixmap = None
dataSet['fragCoords'] = None
if update: if update:
self.updateSpots(dataSet) self.updateSpots(dataSet)
@ -285,8 +442,9 @@ class ScatterPlotItem(GraphicsObject):
dataSet['brush'] = brushes dataSet['brush'] = brushes
else: else:
self.opts['brush'] = fn.mkBrush(*args, **kargs) self.opts['brush'] = fn.mkBrush(*args, **kargs)
self._spotPixmap = None #self._spotPixmap = None
dataSet['fragCoords'] = None
if update: if update:
self.updateSpots(dataSet) self.updateSpots(dataSet)
@ -307,6 +465,7 @@ class ScatterPlotItem(GraphicsObject):
self.opts['symbol'] = symbol self.opts['symbol'] = symbol
self._spotPixmap = None self._spotPixmap = None
dataSet['fragCoords'] = None
if update: if update:
self.updateSpots(dataSet) self.updateSpots(dataSet)
@ -327,6 +486,7 @@ class ScatterPlotItem(GraphicsObject):
self.opts['size'] = size self.opts['size'] = size
self._spotPixmap = None self._spotPixmap = None
dataSet['fragCoords'] = None
if update: if update:
self.updateSpots(dataSet) self.updateSpots(dataSet)
@ -346,34 +506,71 @@ class ScatterPlotItem(GraphicsObject):
else: else:
dataSet['data'] = data dataSet['data'] = data
def setPxMode(self, mode, update=True): def setPxMode(self, mode):
if self.opts['pxMode'] == mode: if self.opts['pxMode'] == mode:
return return
self.opts['pxMode'] = mode self.opts['pxMode'] = mode
self.clearItems() self.invalidate()
if update:
self.generateSpotItems()
def updateSpots(self, dataSet=None): def updateSpots(self, dataSet=None):
if dataSet is None: if dataSet is None:
dataSet = self.data dataSet = self.data
self._maxSpotWidth = 0 self._maxSpotWidth = 0
self._maxSpotPxWidth = 0 self._maxSpotPxWidth = 0
for spot in dataSet['item']: invalidate = False
spot.updateItem()
self.measureSpotSizes(dataSet) self.measureSpotSizes(dataSet)
if self.opts['pxMode']:
mask = np.equal(dataSet['fragCoords'], None)
if np.any(mask):
invalidate = True
opts = self.getSpotOpts(dataSet[mask])
coords = self.fragmentAtlas.getSymbolCoords(opts)
dataSet['fragCoords'][mask] = coords
#for rec in dataSet:
#if rec['fragCoords'] is None:
#invalidate = True
#rec['fragCoords'] = self.fragmentAtlas.getSymbolCoords(*self.getSpotOpts(rec))
if invalidate:
self.invalidate()
def getSpotOpts(self, recs):
if recs.ndim == 0:
rec = recs
symbol = rec['symbol']
if symbol is None:
symbol = self.opts['symbol']
size = rec['size']
if size < 0:
size = self.opts['size']
pen = rec['pen']
if pen is None:
pen = self.opts['pen']
brush = rec['brush']
if brush is None:
brush = self.opts['brush']
return (symbol, size, fn.mkPen(pen), fn.mkBrush(brush))
else:
recs = recs.copy()
recs['symbol'][np.equal(recs['symbol'], None)] = self.opts['symbol']
recs['size'][np.equal(recs['size'], -1)] = self.opts['size']
recs['pen'][np.equal(recs['pen'], None)] = fn.mkPen(self.opts['pen'])
recs['brush'][np.equal(recs['brush'], None)] = fn.mkBrush(self.opts['brush'])
return recs
def measureSpotSizes(self, dataSet): def measureSpotSizes(self, dataSet):
for spot in dataSet['item']: for rec in dataSet:
## keep track of the maximum spot size and pixel size ## keep track of the maximum spot size and pixel size
symbol, size, pen, brush = self.getSpotOpts(rec)
width = 0 width = 0
pxWidth = 0 pxWidth = 0
pen = spot.pen()
if self.opts['pxMode']: if self.opts['pxMode']:
pxWidth = spot.size() + pen.width() pxWidth = size + pen.width()
else: else:
width = spot.size() width = size
if pen.isCosmetic(): if pen.isCosmetic():
pxWidth += pen.width() pxWidth += pen.width()
else: else:
@ -385,20 +582,11 @@ class ScatterPlotItem(GraphicsObject):
def clear(self): def clear(self):
"""Remove all spots from the scatter plot""" """Remove all spots from the scatter plot"""
self.clearItems() #self.clearItems()
self.data = np.empty(0, dtype=self.data.dtype) self.data = np.empty(0, dtype=self.data.dtype)
self.bounds = [None, None] self.bounds = [None, None]
self.invalidate()
def clearItems(self):
for i in self.data['item']:
if i is None:
continue
i.setParentItem(None)
s = i.scene()
if s is not None:
s.removeItem(i)
self.data['item'] = None
def dataBounds(self, ax, frac=1.0, orthoRange=None): def dataBounds(self, ax, frac=1.0, orthoRange=None):
if frac >= 1.0 and self.bounds[ax] is not None: if frac >= 1.0 and self.bounds[ax] is not None:
return self.bounds[ax] return self.bounds[ax]
@ -436,28 +624,12 @@ class ScatterPlotItem(GraphicsObject):
else: else:
return (scipy.stats.scoreatpercentile(d, 50 - (frac * 50)), scipy.stats.scoreatpercentile(d, 50 + (frac * 50))) return (scipy.stats.scoreatpercentile(d, 50 - (frac * 50)), scipy.stats.scoreatpercentile(d, 50 + (frac * 50)))
#def defaultSpotPixmap(self):
### Return the default spot pixmap
#if self._spotPixmap is None:
def generateSpotItems(self): #self._spotPixmap = makeSymbolPixmap(size=self.opts['size'], brush=self.opts['brush'], pen=self.opts['pen'], symbol=self.opts['symbol'])
if self.opts['pxMode']: #return self._spotPixmap
for rec in self.data:
if rec['item'] is None:
rec['item'] = PixmapSpotItem(rec, self)
else:
for rec in self.data:
if rec['item'] is None:
rec['item'] = PathSpotItem(rec, self)
self.measureSpotSizes(self.data)
self.sigPlotChanged.emit(self)
def defaultSpotPixmap(self):
## Return the default spot pixmap
if self._spotPixmap is None:
self._spotPixmap = makeSymbolPixmap(size=self.opts['size'], brush=self.opts['brush'], pen=self.opts['pen'], symbol=self.opts['symbol'])
return self._spotPixmap
def boundingRect(self): def boundingRect(self):
(xmn, xmx) = self.dataBounds(ax=0) (xmn, xmx) = self.dataBounds(ax=0)
@ -470,19 +642,72 @@ class ScatterPlotItem(GraphicsObject):
ymx = 0 ymx = 0
return QtCore.QRectF(xmn, ymn, xmx-xmn, ymx-ymn) return QtCore.QRectF(xmn, ymn, xmx-xmn, ymx-ymn)
def viewRangeChanged(self): def viewTransformChanged(self):
self.prepareGeometryChange() self.prepareGeometryChange()
GraphicsObject.viewRangeChanged(self) GraphicsObject.viewTransformChanged(self)
self.bounds = [None, None] self.bounds = [None, None]
self.fragments = None
def generateFragments(self):
tr = self.deviceTransform()
if tr is None:
return
pts = np.empty((2,len(self.data['x'])))
pts[0] = self.data['x']
pts[1] = self.data['y']
pts = fn.transformCoordinates(tr, pts)
self.fragments = []
for i in xrange(len(self.data)):
rec = self.data[i]
pos = QtCore.QPointF(pts[0,i], pts[1,i])
x,y,w,h = rec['fragCoords']
rect = QtCore.QRectF(y, x, h, w)
self.fragments.append(QtGui.QPainter.PixmapFragment.create(pos, rect))
def paint(self, p, *args): def paint(self, p, *args):
## NOTE: self.paint is disabled by this line in __init__: #p.setPen(fn.mkPen('r'))
## self.setFlag(self.ItemHasNoContents, True) #p.drawRect(self.boundingRect())
p.setPen(fn.mkPen('r')) if self.opts['pxMode']:
p.drawRect(self.boundingRect()) atlas = self.fragmentAtlas.getAtlas()
#arr = fn.imageToArray(atlas.toImage(), copy=True)
#if hasattr(self, 'lastAtlas'):
#if np.any(self.lastAtlas != arr):
#print "Atlas changed:", arr
#self.lastAtlas = arr
if self.fragments is None:
self.updateSpots()
self.generateFragments()
p.resetTransform()
if not USE_PYSIDE and self.opts['useCache']:
p.drawPixmapFragments(self.fragments, atlas)
else:
for i in range(len(self.data)):
rec = self.data[i]
frag = self.fragments[i]
p.resetTransform()
p.translate(frag.x, frag.y)
drawSymbol(p, *self.getSpotOpts(rec))
else:
if self.picture is None:
self.picture = QtGui.QPicture()
p2 = QtGui.QPainter(self.picture)
for rec in self.data:
p2.resetTransform()
p2.translate(rec['x'], rec['y'])
drawSymbol(p2, *self.getSpotOpts(rec))
p2.end()
self.picture.play(p)
def points(self): def points(self):
for rec in self.data:
if rec['item'] is None:
rec['item'] = SpotItem(rec, self)
return self.data['item'] return self.data['item']
def pointsAt(self, pos): def pointsAt(self, pos):
@ -506,8 +731,8 @@ class ScatterPlotItem(GraphicsObject):
#else: #else:
#print "No hit:", (x, y), (sx, sy) #print "No hit:", (x, y), (sx, sy)
#print " ", (sx-s2x, sy-s2y), (sx+s2x, sy+s2y) #print " ", (sx-s2x, sy-s2y), (sx+s2x, sy+s2y)
pts.sort(lambda a,b: cmp(b.zValue(), a.zValue())) #pts.sort(lambda a,b: cmp(b.zValue(), a.zValue()))
return pts return pts[::-1]
def mouseClickEvent(self, ev): def mouseClickEvent(self, ev):
@ -524,7 +749,7 @@ class ScatterPlotItem(GraphicsObject):
ev.ignore() ev.ignore()
class SpotItem(GraphicsItem): class SpotItem(object):
""" """
Class referring to individual spots in a scatter plot. Class referring to individual spots in a scatter plot.
These can be retrieved by calling ScatterPlotItem.points() or These can be retrieved by calling ScatterPlotItem.points() or
@ -532,14 +757,12 @@ class SpotItem(GraphicsItem):
""" """
def __init__(self, data, plot): def __init__(self, data, plot):
GraphicsItem.__init__(self, register=False) #GraphicsItem.__init__(self, register=False)
self._data = data self._data = data
self._plot = plot self._plot = plot
#self._viewBox = None #self.setParentItem(plot)
#self._viewWidget = None #self.setPos(QtCore.QPointF(data['x'], data['y']))
self.setParentItem(plot) #self.updateItem()
self.setPos(QtCore.QPointF(data['x'], data['y']))
self.updateItem()
def data(self): def data(self):
"""Return the user data associated with this spot.""" """Return the user data associated with this spot."""
@ -553,6 +776,12 @@ class SpotItem(GraphicsItem):
else: else:
return self._data['size'] return self._data['size']
def pos(self):
return Point(self._data['x'], self._data['y'])
def viewPos(self):
return self._plot.mapToView(self.pos())
def setSize(self, size): def setSize(self, size):
"""Set the size of this spot. """Set the size of this spot.
If the size is set to -1, then the ScatterPlotItem's default size If the size is set to -1, then the ScatterPlotItem's default size
@ -618,37 +847,41 @@ class SpotItem(GraphicsItem):
"""Set the user-data associated with this spot""" """Set the user-data associated with this spot"""
self._data['data'] = data self._data['data'] = data
class PixmapSpotItem(SpotItem, QtGui.QGraphicsPixmapItem):
def __init__(self, data, plot):
QtGui.QGraphicsPixmapItem.__init__(self)
self.setFlags(self.flags() | self.ItemIgnoresTransformations)
SpotItem.__init__(self, data, plot)
def setPixmap(self, pixmap):
QtGui.QGraphicsPixmapItem.setPixmap(self, pixmap)
self.setOffset(-pixmap.width()/2.+0.5, -pixmap.height()/2.)
def updateItem(self): def updateItem(self):
symbolOpts = (self._data['pen'], self._data['brush'], self._data['size'], self._data['symbol']) self._data['fragCoords'] = None
self._plot.updateSpots([self._data])
self._plot.invalidate()
#class PixmapSpotItem(SpotItem, QtGui.QGraphicsPixmapItem):
#def __init__(self, data, plot):
#QtGui.QGraphicsPixmapItem.__init__(self)
#self.setFlags(self.flags() | self.ItemIgnoresTransformations)
#SpotItem.__init__(self, data, plot)
#def setPixmap(self, pixmap):
#QtGui.QGraphicsPixmapItem.setPixmap(self, pixmap)
#self.setOffset(-pixmap.width()/2.+0.5, -pixmap.height()/2.)
#def updateItem(self):
#symbolOpts = (self._data['pen'], self._data['brush'], self._data['size'], self._data['symbol'])
## If all symbol options are default, use default pixmap ### If all symbol options are default, use default pixmap
if symbolOpts == (None, None, -1, ''): #if symbolOpts == (None, None, -1, ''):
pixmap = self._plot.defaultSpotPixmap() #pixmap = self._plot.defaultSpotPixmap()
else: #else:
pixmap = makeSymbolPixmap(size=self.size(), pen=self.pen(), brush=self.brush(), symbol=self.symbol()) #pixmap = makeSymbolPixmap(size=self.size(), pen=self.pen(), brush=self.brush(), symbol=self.symbol())
self.setPixmap(pixmap) #self.setPixmap(pixmap)
class PathSpotItem(SpotItem, QtGui.QGraphicsPathItem): #class PathSpotItem(SpotItem, QtGui.QGraphicsPathItem):
def __init__(self, data, plot): #def __init__(self, data, plot):
QtGui.QGraphicsPathItem.__init__(self) #QtGui.QGraphicsPathItem.__init__(self)
SpotItem.__init__(self, data, plot) #SpotItem.__init__(self, data, plot)
def updateItem(self): #def updateItem(self):
QtGui.QGraphicsPathItem.setPath(self, Symbols[self.symbol()]) #QtGui.QGraphicsPathItem.setPath(self, Symbols[self.symbol()])
QtGui.QGraphicsPathItem.setPen(self, self.pen()) #QtGui.QGraphicsPathItem.setPen(self, self.pen())
QtGui.QGraphicsPathItem.setBrush(self, self.brush()) #QtGui.QGraphicsPathItem.setBrush(self, self.brush())
size = self.size() #size = self.size()
self.resetTransform() #self.resetTransform()
self.scale(size, size) #self.scale(size, size)

View File

@ -48,6 +48,7 @@ class ViewBox(GraphicsWidget):
sigRangeChanged = QtCore.Signal(object, object) sigRangeChanged = QtCore.Signal(object, object)
#sigActionPositionChanged = QtCore.Signal(object) #sigActionPositionChanged = QtCore.Signal(object)
sigStateChanged = QtCore.Signal(object) sigStateChanged = QtCore.Signal(object)
sigTransformChanged = QtCore.Signal(object)
## mouse modes ## mouse modes
PanMode = 3 PanMode = 3
@ -307,10 +308,6 @@ class ViewBox(GraphicsWidget):
print("make qrectf failed:", self.state['viewRange']) print("make qrectf failed:", self.state['viewRange'])
raise raise
#def viewportTransform(self):
##return self.itemTransform(self.childGroup)[0]
#return self.childGroup.itemTransform(self)[0]
def targetRange(self): def targetRange(self):
return [x[:] for x in self.state['targetRange']] ## return copy return [x[:] for x in self.state['targetRange']] ## return copy
@ -554,10 +551,10 @@ class ViewBox(GraphicsWidget):
## Make corrections to range ## Make corrections to range
xr = childRange[ax] xr = childRange[ax]
if xr is not None: if xr is not None:
if self.state['autoPan'][0]: if self.state['autoPan'][ax]:
x = sum(xr) * 0.5 x = sum(xr) * 0.5
#x = childRect.center().x() #x = childRect.center().x()
w2 = (targetRect[0][1]-targetRect[0][0]) / 2. w2 = (targetRect[ax][1]-targetRect[ax][0]) / 2.
#childRect.setLeft(x-w2) #childRect.setLeft(x-w2)
#childRect.setRight(x+w2) #childRect.setRight(x+w2)
childRange[ax] = [x-w2, x+w2] childRange[ax] = [x-w2, x+w2]
@ -1127,29 +1124,15 @@ class ViewBox(GraphicsWidget):
m = QtGui.QTransform() m = QtGui.QTransform()
## First center the viewport at 0 ## First center the viewport at 0
#self.childGroup.resetTransform()
#self.resetTransform()
#center = self.transform().inverted()[0].map(bounds.center())
center = bounds.center() center = bounds.center()
#print " transform to center:", center
#if self.state['yInverted']:
#m.translate(center.x(), -center.y())
#print " inverted; translate", center.x(), center.y()
#else:
m.translate(center.x(), center.y()) m.translate(center.x(), center.y())
#print " not inverted; translate", center.x(), -center.y()
## Now scale and translate properly ## Now scale and translate properly
m.scale(scale[0], scale[1]) m.scale(scale[0], scale[1])
st = Point(vr.center()) st = Point(vr.center())
#st = translate
m.translate(-st[0], -st[1]) m.translate(-st[0], -st[1])
self.childGroup.setTransform(m) self.childGroup.setTransform(m)
#self.setTransform(m)
#self.prepareGeometryChange()
#self.currentScale = scale
if changed[0]: if changed[0]:
self.sigXRangeChanged.emit(self, tuple(self.state['viewRange'][0])) self.sigXRangeChanged.emit(self, tuple(self.state['viewRange'][0]))
@ -1157,6 +1140,8 @@ class ViewBox(GraphicsWidget):
self.sigYRangeChanged.emit(self, tuple(self.state['viewRange'][1])) self.sigYRangeChanged.emit(self, tuple(self.state['viewRange'][1]))
if any(changed): if any(changed):
self.sigRangeChanged.emit(self, self.state['viewRange']) self.sigRangeChanged.emit(self, self.state['viewRange'])
self.sigTransformChanged.emit(self)
def paint(self, p, opt, widget): def paint(self, p, opt, widget):
if self.border is not None: if self.border is not None:
@ -1165,20 +1150,6 @@ class ViewBox(GraphicsWidget):
#p.fillRect(bounds, QtGui.QColor(0, 0, 0)) #p.fillRect(bounds, QtGui.QColor(0, 0, 0))
p.drawPath(bounds) p.drawPath(bounds)
#def saveSvg(self):
#pass
#def saveImage(self):
#pass
#def savePrint(self):
#printer = QtGui.QPrinter()
#if QtGui.QPrintDialog(printer).exec_() == QtGui.QDialog.Accepted:
#p = QtGui.QPainter(printer)
#p.setRenderHint(p.Antialiasing)
#self.scene().render(p)
#p.end()
def updateBackground(self): def updateBackground(self):
bg = self.state['background'] bg = self.state['background']
if bg is None: if bg is None:

View File

@ -21,13 +21,13 @@ def mkQApp():
class GraphicsWindow(GraphicsLayoutWidget): class GraphicsWindow(GraphicsLayoutWidget):
def __init__(self, title=None, size=(800,600), **kargs): def __init__(self, title=None, size=(800,600), **kargs):
mkQApp() mkQApp()
self.win = QtGui.QMainWindow() #self.win = QtGui.QMainWindow()
GraphicsLayoutWidget.__init__(self, **kargs) GraphicsLayoutWidget.__init__(self, **kargs)
self.win.setCentralWidget(self) #self.win.setCentralWidget(self)
self.win.resize(*size) self.resize(*size)
if title is not None: if title is not None:
self.win.setWindowTitle(title) self.setWindowTitle(title)
self.win.show() self.show()
class TabWindow(QtGui.QMainWindow): class TabWindow(QtGui.QMainWindow):

View File

@ -0,0 +1,87 @@
from OpenGL.GL import *
from .. GLGraphicsItem import GLGraphicsItem
from pyqtgraph.Qt import QtGui
import numpy as np
__all__ = ['GLImageItem']
class GLImageItem(GLGraphicsItem):
"""
**Bases:** :class:`GLGraphicsItem <pyqtgraph.opengl.GLGraphicsItem>`
Displays image data as a textured quad.
"""
def __init__(self, data, smooth=False):
"""
============== =======================================================================================
**Arguments:**
data Volume data to be rendered. *Must* be 3D numpy array (x, y, RGBA) with dtype=ubyte.
(See functions.makeRGBA)
smooth (bool) If True, the volume slices are rendered with linear interpolation
============== =======================================================================================
"""
self.smooth = smooth
self.data = data
GLGraphicsItem.__init__(self)
def initializeGL(self):
glEnable(GL_TEXTURE_2D)
self.texture = glGenTextures(1)
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.data.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.data.transpose((1,0,2)))
glDisable(GL_TEXTURE_2D)
#self.lists = {}
#for ax in [0,1,2]:
#for d in [-1, 1]:
#l = glGenLists(1)
#self.lists[(ax,d)] = l
#glNewList(l, GL_COMPILE)
#self.drawVolume(ax, d)
#glEndList()
def paint(self):
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 )
glColor4f(1,1,1,1)
glBegin(GL_QUADS)
glTexCoord2f(0,0)
glVertex3f(0,0,0)
glTexCoord2f(1,0)
glVertex3f(self.data.shape[0], 0, 0)
glTexCoord2f(1,1)
glVertex3f(self.data.shape[0], self.data.shape[1], 0)
glTexCoord2f(0,1)
glVertex3f(0, self.data.shape[1], 0)
glEnd()
glDisable(GL_TEXTURE_3D)

View File

@ -8,31 +8,47 @@ __all__ = ['GLScatterPlotItem']
class GLScatterPlotItem(GLGraphicsItem): class GLScatterPlotItem(GLGraphicsItem):
"""Draws points at a list of 3D positions.""" """Draws points at a list of 3D positions."""
def __init__(self, data=None): def __init__(self, **kwds):
GLGraphicsItem.__init__(self) GLGraphicsItem.__init__(self)
self.data = [] self.pos = []
if data is not None: self.size = 10
self.setData(data) self.color = [1.0,1.0,1.0,0.5]
self.pxMode = True
self.setData(**kwds)
def setData(self, data): def setData(self, **kwds):
""" """
Data may be either a list of dicts (one dict per point) or a numpy record array. Update the data displayed by this item. All arguments are optional;
for example it is allowed to update spot positions while leaving
colors unchanged, etc.
==================== ================================================== ==================== ==================================================
Allowed fields are: Arguments:
------------------------------------------------------------------------ ------------------------------------------------------------------------
pos (x,y,z) tuple of coordinate values or QVector3D pos (N,3) array of floats specifying point locations.
color (r,g,b,a) tuple of floats (0.0-1.0) or QColor color (N,4) array of floats (0.0-1.0) specifying
size (float) diameter of spot in pixels spot colors OR a tuple of floats specifying
a single color for all spots.
size (N,) array of floats specifying spot sizes or
a single value to apply to all spots.
pxMode If True, spot sizes are expressed in pixels.
Otherwise, they are expressed in item coordinates.
==================== ================================================== ==================== ==================================================
""" """
args = ['pos', 'color', 'size', 'pxMode']
for k in kwds.keys():
self.data = data if k not in args:
raise Exception('Invalid keyword argument: %s (allowed arguments are %s)' % (k, str(args)))
self.pos = kwds.get('pos', self.pos)
self.color = kwds.get('color', self.color)
self.size = kwds.get('size', self.size)
self.pxMode = kwds.get('pxMode', self.pxMode)
self.update() self.update()
def initializeGL(self): def initializeGL(self):
## Generate texture for rendering points
w = 64 w = 64
def fn(x,y): def fn(x,y):
r = ((x-w/2.)**2 + (y-w/2.)**2) ** 0.5 r = ((x-w/2.)**2 + (y-w/2.)**2) ** 0.5
@ -73,28 +89,49 @@ class GLScatterPlotItem(GLGraphicsItem):
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE) glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE)
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE) glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE)
for pt in self.data: if self.pxMode:
pos = pt['pos'] glVertexPointerf(self.pos)
try: if isinstance(self.color, np.ndarray):
color = pt['color'] glColorPointerf(self.color)
except KeyError: else:
color = (1,1,1,1) if isinstance(self.color, QtGui.QColor):
try: glColor4f(*fn.glColor(self.color))
size = pt['size'] else:
except KeyError: glColor4f(*self.color)
size = 10
if isinstance(color, QtGui.QColor):
color = fn.glColor(color)
pxSize = self.view().pixelSize(QtGui.QVector3D(*pos))
glPointSize(size / pxSize) if isinstance(self.size, np.ndarray):
glBegin( GL_POINTS ) raise Exception('Array size not yet supported in pxMode (hopefully soon)')
glColor4f(*color) # x is blue
#glNormal3f(size, 0, 0) glPointSize(self.size)
glVertex3f(*pos) glEnableClientState(GL_VERTEX_ARRAY)
glEnd() glEnableClientState(GL_COLOR_ARRAY)
glDrawArrays(GL_POINTS, 0, len(self.pos))
else:
for i in range(len(self.pos)):
pos = self.pos[i]
if isinstance(self.color, np.ndarray):
color = self.color[i]
else:
color = self.color
if isinstance(self.color, QtGui.QColor):
color = fn.glColor(self.color)
if isinstance(self.size, np.ndarray):
size = self.size[i]
else:
size = self.size
pxSize = self.view().pixelSize(QtGui.QVector3D(*pos))
glPointSize(size / pxSize)
glBegin( GL_POINTS )
glColor4f(*color) # x is blue
#glNormal3f(size, 0, 0)
glVertex3f(*pos)
glEnd()

View File

@ -42,6 +42,7 @@ class GraphicsView(QtGui.QGraphicsView):
enabled via enableMouse() (but ordinarily, we use ViewBox for this functionality).""" enabled via enableMouse() (but ordinarily, we use ViewBox for this functionality)."""
sigRangeChanged = QtCore.Signal(object, object) sigRangeChanged = QtCore.Signal(object, object)
sigTransformChanged = QtCore.Signal(object)
sigMouseReleased = QtCore.Signal(object) sigMouseReleased = QtCore.Signal(object)
sigSceneMouseMoved = QtCore.Signal(object) sigSceneMouseMoved = QtCore.Signal(object)
#sigRegionChanged = QtCore.Signal(object) #sigRegionChanged = QtCore.Signal(object)
@ -212,6 +213,7 @@ class GraphicsView(QtGui.QGraphicsView):
self.fitInView(self.range, QtCore.Qt.IgnoreAspectRatio) self.fitInView(self.range, QtCore.Qt.IgnoreAspectRatio)
self.sigRangeChanged.emit(self, self.range) self.sigRangeChanged.emit(self, self.range)
self.sigTransformChanged.emit(self)
if propagate: if propagate:
for v in self.lockedViewports: for v in self.lockedViewports: