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'
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
#self._stretch = (x, y)
if x is None:
@ -100,6 +105,10 @@ class Dock(QtGui.QWidget, DockDrop):
#return self._stretch
def hideTitleBar(self):
"""
Hide the title bar for this Dock.
This will prevent the Dock being moved by the user.
"""
self.label.hide()
self.labelHidden = True
if 'center' in self.allowedAreas:
@ -107,12 +116,21 @@ class Dock(QtGui.QWidget, DockDrop):
self.updateStyle()
def showTitleBar(self):
"""
Show the title bar for this Dock.
"""
self.label.show()
self.labelHidden = False
self.allowedAreas.add('center')
self.updateStyle()
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
if o == 'auto' and self.autoOrient:
if self.container().type() == 'tab':
@ -127,6 +145,7 @@ class Dock(QtGui.QWidget, DockDrop):
self.updateStyle()
def updateStyle(self):
## updates orientation and appearance of title bar
#print self.name(), "update style:", self.orientation, self.moveLabel, self.label.isVisible()
if self.labelHidden:
self.widgetArea.setStyleSheet(self.nStyle)
@ -154,6 +173,10 @@ class Dock(QtGui.QWidget, DockDrop):
return self._container
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:
row = self.currentRow
self.currentRow = max(row+1, self.currentRow)
@ -188,7 +211,8 @@ class Dock(QtGui.QWidget, DockDrop):
def __repr__(self):
return "<Dock %s %s>" % (self.name(), self.stretch())
class DockLabel(VerticalLabel):
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):
"""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.
## If there is no neighbor, then the container is the top.
@ -90,6 +100,17 @@ class DockArea(Container, QtGui.QWidget, DockDrop):
dock.area = self
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):
if obj is None:
return self
@ -131,13 +152,6 @@ class DockArea(Container, QtGui.QWidget, DockDrop):
return 0
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):
#self.drawDockOverlay()
@ -159,6 +173,7 @@ class DockArea(Container, QtGui.QWidget, DockDrop):
return area
def floatDock(self, dock):
"""Removes *dock* from this DockArea and places it in a new window."""
area = self.addTempArea()
area.win.resize(dock.size())
area.moveDock(dock, 'top', None)
@ -170,6 +185,9 @@ class DockArea(Container, QtGui.QWidget, DockDrop):
area.window().close()
def saveState(self):
"""
Return a serialized (storable) representation of the state of
all Docks in this DockArea."""
state = {'main': self.childState(self.topContainer), 'float': []}
for a in self.tempAreas:
geo = a.win.geometry()
@ -188,6 +206,10 @@ class DockArea(Container, QtGui.QWidget, DockDrop):
def restoreState(self, state):
"""
Restore Dock configuration as generated by saveState.
"""
## 1) make dict of all docks and list of existing containers
containers, docks = self.findAll()
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
glmeshitem
glvolumeitem
glimageitem
glaxisitem
glgraphicsitem
glscatterplotitem
meshdata

View File

@ -29,6 +29,7 @@ Contents:
scalebar
labelitem
vtickgroup
legenditem
gradienteditoritem
histogramlutitem
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
import pyqtgraph.opengl as gl
import numpy as np
app = QtGui.QApplication([])
w = gl.GLViewWidget()
@ -14,18 +15,47 @@ w.show()
g = gl.GLGridItem()
w.addItem(g)
pts = [
{'pos': (1,0,0), 'size':0.5, 'color':(1.0, 0.0, 0.0, 0.5)},
{'pos': (0,1,0), 'size':0.2, 'color':(0.0, 0.0, 1.0, 0.5)},
{'pos': (0,0,1), 'size':2./3., 'color':(0.0, 1.0, 0.0, 0.5)},
]
z = 0.5
d = 6.0
for i in range(50):
pts.append({'pos': (0,0,z), 'size':2./d, 'color':(0.0, 1.0, 0.0, 0.5)})
z *= 0.5
d *= 2.0
sp = gl.GLScatterPlotItem(pts)
#pos = np.empty((53, 3))
#size = np.empty((53))
#color = np.empty((53, 4))
#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)
#pos[2] = (0,0,1); size[2] = 2./3.; color[2] = (0.0, 1.0, 0.0, 0.5)
#z = 0.5
#d = 6.0
#for i in range(3,53):
#pos[i] = (0,0,z)
#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)
## 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
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
lastTime = time()
fps = None
@ -49,7 +49,8 @@ def update():
s = np.clip(dt*3., 0, 1)
fps = fps * (1-s) + (1.0/dt) * s
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.timeout.connect(update)
timer.start(0)

View File

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

View File

@ -39,6 +39,24 @@
</column>
</widget>
</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>
<widget class="QPushButton" name="loadBtn">
<property name="text">

View File

@ -2,7 +2,7 @@
# 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
#
# 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.header().setVisible(False)
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.setObjectName(_fromUtf8("loadBtn"))
self.verticalLayout.addWidget(self.loadBtn)
@ -51,5 +60,7 @@ class Ui_Form(object):
def retranslateUi(self, Form):
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))

View File

@ -2,7 +2,7 @@
# 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
#
# 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.header().setVisible(False)
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.setObjectName("loadBtn")
self.verticalLayout.addWidget(self.loadBtn)
@ -46,5 +55,7 @@ class Ui_Form(object):
def retranslateUi(self, Form):
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))

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
import sys, os
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):
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:
self.fileSaveDialog(filter=["*.csv", "*.tsv"])

View File

@ -54,8 +54,13 @@ class Exporter(object):
fileName = str(fileName)
global LastExportDirectory
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):
if isinstance(self.item, pg.GraphicsScene):

View File

@ -63,7 +63,7 @@ class ImageExporter(Exporter):
self.getScene().render(painter, QtCore.QRectF(targetRect), sourceRect)
finally:
self.setExportMode(False)
self.png.save(fileName)
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 decimal, re
import ctypes
try:
import scipy.ndimage
@ -223,13 +224,15 @@ def mkColor(*args):
return QtGui.QColor(*args)
def mkBrush(*args):
def mkBrush(*args, **kwds):
"""
| Convenience function for constructing Brush.
| This function always constructs a solid brush and accepts the same arguments as :func:`mkColor() <pyqtgraph.mkColor>`
| 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]
if arg is None:
return QtGui.QBrush(QtCore.Qt.NoBrush)
@ -237,7 +240,7 @@ def mkBrush(*args):
return QtGui.QBrush(arg)
else:
color = arg
if len(args) > 1:
elif len(args) > 1:
color = args
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):
"""
@ -605,7 +611,7 @@ def makeARGB(data, lut=None, levels=None, useRGBA=False):
Lookup tables can be built using GradientWidget.
levels - List [min, max]; optionally rescale data before converting through the
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.
"""
@ -779,30 +785,117 @@ def makeARGB(data, lut=None, levels=None, useRGBA=False):
return imgData, alpha
def makeQImage(imgData, alpha):
"""Turn an ARGB array into QImage"""
def makeQImage(imgData, alpha=None, copy=True, transpose=True):
"""
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
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:
imgFormat = QtGui.QImage.Format_ARGB32
else:
imgFormat = QtGui.QImage.Format_RGB32
imgData = imgData.transpose((1, 0, 2)) ## QImage expects the row/column order to be opposite
try:
buf = imgData.data
except AttributeError: ## happens when image data is non-contiguous
if transpose:
imgData = imgData.transpose((1, 0, 2)) ## QImage expects the row/column order to be opposite
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)
buf = imgData.data
copied = True
prof.mark('1')
qimage = QtGui.QImage(buf, imgData.shape[1], imgData.shape[0], imgFormat)
prof.mark('2')
qimage.data = imgData
prof.finish()
return qimage
if copy is True and copied is False:
imgData = imgData.copy()
if USE_PYSIDE:
ch = ctypes.c_char.from_buffer(imgData, 0)
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):
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])
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)
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.label.isVisible():
@ -182,6 +184,8 @@ class AxisItem(GraphicsWidget):
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:
w = max(0, self.tickLength) + 40
if self.label.isVisible():

View File

@ -394,14 +394,17 @@ class GraphicsItem(object):
if oldView is not None:
#print "disconnect:", self, oldView
oldView.sigRangeChanged.disconnect(self.viewRangeChanged)
oldView.sigTransformChanged.disconnect(self.viewTransformChanged)
self._connectedView = None
## connect to new view
if view is not None:
#print "connect:", self, view
view.sigRangeChanged.connect(self.viewRangeChanged)
view.sigTransformChanged.connect(self.viewTransformChanged)
self._connectedView = weakref.ref(view)
self.viewRangeChanged()
self.viewTransformChanged()
## inform children that their view might have changed
self._replaceView(oldView)
@ -425,3 +428,9 @@ class GraphicsItem(object):
Called whenever the view coordinates of the ViewBox containing this item have changed.
"""
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,
'fillLevel': None,
'brush': None,
'stepMode': False,
}
self.setClickable(kargs.get('clickable', False))
self.setData(*args, **kargs)
@ -223,8 +224,15 @@ class PlotCurveItem(GraphicsObject):
prof.mark('copy')
if self.xData.shape != self.yData.shape:
raise Exception("X and Y arrays must be the same shape--got %s and %s." % (str(x.shape), str(y.shape)))
if 'stepMode' in kargs:
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.fillPath = None
@ -267,6 +275,29 @@ class PlotCurveItem(GraphicsObject):
## 0(i4)
##
## 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??
n = x.shape[0]
# create empty array, pad with extra space on either end
@ -324,12 +355,21 @@ class PlotCurveItem(GraphicsObject):
pixels = self.pixelVectors()
if pixels == (None, None):
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)
def paint(self, p, opt, widget):

View File

@ -130,6 +130,8 @@ class PlotDataItem(GraphicsObject):
'symbolBrush': (50, 50, 150),
'pxMode': True,
'pointMode': None,
'data': None,
}
self.setData(*args, **kargs)
@ -144,22 +146,30 @@ class PlotDataItem(GraphicsObject):
return QtCore.QRectF() ## let child items handle this
def setAlpha(self, alpha, auto):
if self.opts['alphaHint'] == alpha and self.opts['alphaMode'] == auto:
return
self.opts['alphaHint'] = alpha
self.opts['alphaMode'] = auto
self.setOpacity(alpha)
#self.update()
def setFftMode(self, mode):
if self.opts['fftMode'] == mode:
return
self.opts['fftMode'] = mode
self.xDisp = self.yDisp = None
self.updateItems()
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.updateItems()
def setPointMode(self, mode):
if self.opts['pointMode'] == mode:
return
self.opts['pointMode'] = mode
self.update()
@ -193,6 +203,8 @@ class PlotDataItem(GraphicsObject):
def setFillBrush(self, *args, **kargs):
brush = fn.mkBrush(*args, **kargs)
if self.opts['fillBrush'] == brush:
return
self.opts['fillBrush'] = brush
self.updateItems()
@ -200,16 +212,22 @@ class PlotDataItem(GraphicsObject):
return self.setFillBrush(*args, **kargs)
def setFillLevel(self, level):
if self.opts['fillLevel'] == level:
return
self.opts['fillLevel'] = level
self.updateItems()
def setSymbol(self, symbol):
if self.opts['symbol'] == symbol:
return
self.opts['symbol'] = symbol
#self.scatter.setSymbol(symbol)
self.updateItems()
def setSymbolPen(self, *args, **kargs):
pen = fn.mkPen(*args, **kargs)
if self.opts['symbolPen'] == pen:
return
self.opts['symbolPen'] = pen
#self.scatter.setSymbolPen(pen)
self.updateItems()
@ -218,21 +236,26 @@ class PlotDataItem(GraphicsObject):
def setSymbolBrush(self, *args, **kargs):
brush = fn.mkBrush(*args, **kargs)
if self.opts['symbolBrush'] == brush:
return
self.opts['symbolBrush'] = brush
#self.scatter.setSymbolBrush(brush)
self.updateItems()
def setSymbolSize(self, size):
if self.opts['symbolSize'] == size:
return
self.opts['symbolSize'] = size
#self.scatter.setSymbolSize(symbolSize)
self.updateItems()
def setDownsampling(self, ds):
if self.opts['downsample'] != ds:
self.opts['downsample'] = ds
self.xDisp = self.yDisp = None
self.updateItems()
if self.opts['downsample'] == ds:
return
self.opts['downsample'] = ds
self.xDisp = self.yDisp = None
self.updateItems()
def setData(self, *args, **kargs):
"""
@ -436,9 +459,12 @@ class PlotDataItem(GraphicsObject):
and max)
=============== =============================================================
"""
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 (0, 0)
return None
if ax == 0:
d = x
@ -450,14 +476,15 @@ class PlotDataItem(GraphicsObject):
if orthoRange is not None:
mask = (d2 >= orthoRange[0]) * (d2 <= orthoRange[1])
d = d[mask]
d2 = d2[mask]
#d2 = d2[mask]
if frac >= 1.0:
return (np.min(d), np.max(d))
elif frac <= 0.0:
raise Exception("Value for parameter 'frac' must be > 0. (got %s)" % str(frac))
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 (scipy.stats.scoreatpercentile(d, 50 - (frac * 50)), scipy.stats.scoreatpercentile(d, 50 + (frac * 50)))
return None
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
import pyqtgraph.functions as fn
from .GraphicsItem import GraphicsItem
@ -32,26 +32,171 @@ for k, c in coords.items():
Symbols[k].lineTo(x, y)
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
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)
p = QtGui.QPainter(image)
p.setRenderHint(p.Antialiasing)
p.translate(image.width()*0.5, image.height()*0.5)
p.scale(size, size)
p.setPen(pen)
p.setBrush(brush)
if isinstance(symbol, basestring):
symbol = Symbols[symbol]
p.drawPath(symbol)
drawSymbol(p, symbol, size, pen, brush)
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):
"""
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)
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._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._spotPixmap = None
self.opts = {'pxMode': True}
self.opts = {'pxMode': True, 'useCache': True} ## If useCache is False, symbols are re-drawn on every paint.
self.setPen(200,200,200, update=False)
self.setBrush(100,100,150, update=False)
@ -96,6 +244,8 @@ class ScatterPlotItem(GraphicsObject):
prof.mark('setData')
prof.finish()
#self.setCacheMode(self.DeviceCoordinateCache)
def setData(self, *args, **kargs):
"""
**Ordered Arguments:**
@ -130,6 +280,7 @@ class ScatterPlotItem(GraphicsObject):
*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.addPoints(*args, **kargs)
@ -183,8 +334,8 @@ class ScatterPlotItem(GraphicsObject):
## note that np.empty initializes object fields to None and string fields to ''
self.data[:len(oldData)] = oldData
for i in range(len(oldData)):
oldData[i]['item']._data = self.data[i] ## Make sure items have proper reference to new array
#for i in range(len(oldData)):
#oldData[i]['item']._data = self.data[i] ## Make sure items have proper reference to new array
newData = self.data[len(oldData):]
newData['size'] = -1 ## indicates to use default size
@ -217,7 +368,7 @@ class ScatterPlotItem(GraphicsObject):
newData['y'] = kargs['y']
if 'pxMode' in kargs:
self.setPxMode(kargs['pxMode'], update=False)
self.setPxMode(kargs['pxMode'])
## Set any extra parameters provided in keyword arguments
for k in ['pen', 'brush', 'symbol', 'size']:
@ -228,12 +379,18 @@ class ScatterPlotItem(GraphicsObject):
if 'data' in kargs:
self.setPointData(kargs['data'], dataSet=newData)
#self.updateSpots()
self.prepareGeometryChange()
self.bounds = [None, None]
self.generateSpotItems()
self.invalidate()
self.updateSpots(newData)
self.sigPlotChanged.emit(self)
def invalidate(self):
## clear any cached drawing state
self.picture = None
self.fragments = None
self.update()
def getData(self):
return self.data['x'], self.data['y']
@ -263,8 +420,8 @@ class ScatterPlotItem(GraphicsObject):
dataSet['pen'] = pens
else:
self.opts['pen'] = fn.mkPen(*args, **kargs)
self._spotPixmap = None
dataSet['fragCoords'] = None
if update:
self.updateSpots(dataSet)
@ -285,8 +442,9 @@ class ScatterPlotItem(GraphicsObject):
dataSet['brush'] = brushes
else:
self.opts['brush'] = fn.mkBrush(*args, **kargs)
self._spotPixmap = None
#self._spotPixmap = None
dataSet['fragCoords'] = None
if update:
self.updateSpots(dataSet)
@ -307,6 +465,7 @@ class ScatterPlotItem(GraphicsObject):
self.opts['symbol'] = symbol
self._spotPixmap = None
dataSet['fragCoords'] = None
if update:
self.updateSpots(dataSet)
@ -327,6 +486,7 @@ class ScatterPlotItem(GraphicsObject):
self.opts['size'] = size
self._spotPixmap = None
dataSet['fragCoords'] = None
if update:
self.updateSpots(dataSet)
@ -346,34 +506,71 @@ class ScatterPlotItem(GraphicsObject):
else:
dataSet['data'] = data
def setPxMode(self, mode, update=True):
def setPxMode(self, mode):
if self.opts['pxMode'] == mode:
return
self.opts['pxMode'] = mode
self.clearItems()
if update:
self.generateSpotItems()
self.invalidate()
def updateSpots(self, dataSet=None):
if dataSet is None:
dataSet = self.data
self._maxSpotWidth = 0
self._maxSpotPxWidth = 0
for spot in dataSet['item']:
spot.updateItem()
invalidate = False
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):
for spot in dataSet['item']:
for rec in dataSet:
## keep track of the maximum spot size and pixel size
symbol, size, pen, brush = self.getSpotOpts(rec)
width = 0
pxWidth = 0
pen = spot.pen()
if self.opts['pxMode']:
pxWidth = spot.size() + pen.width()
pxWidth = size + pen.width()
else:
width = spot.size()
width = size
if pen.isCosmetic():
pxWidth += pen.width()
else:
@ -385,20 +582,11 @@ class ScatterPlotItem(GraphicsObject):
def clear(self):
"""Remove all spots from the scatter plot"""
self.clearItems()
#self.clearItems()
self.data = np.empty(0, dtype=self.data.dtype)
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):
if frac >= 1.0 and self.bounds[ax] is not None:
return self.bounds[ax]
@ -436,28 +624,12 @@ class ScatterPlotItem(GraphicsObject):
else:
return (scipy.stats.scoreatpercentile(d, 50 - (frac * 50)), scipy.stats.scoreatpercentile(d, 50 + (frac * 50)))
def generateSpotItems(self):
if self.opts['pxMode']:
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 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):
(xmn, xmx) = self.dataBounds(ax=0)
@ -470,19 +642,72 @@ class ScatterPlotItem(GraphicsObject):
ymx = 0
return QtCore.QRectF(xmn, ymn, xmx-xmn, ymx-ymn)
def viewRangeChanged(self):
def viewTransformChanged(self):
self.prepareGeometryChange()
GraphicsObject.viewRangeChanged(self)
GraphicsObject.viewTransformChanged(self)
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):
## NOTE: self.paint is disabled by this line in __init__:
## self.setFlag(self.ItemHasNoContents, True)
p.setPen(fn.mkPen('r'))
p.drawRect(self.boundingRect())
#p.setPen(fn.mkPen('r'))
#p.drawRect(self.boundingRect())
if self.opts['pxMode']:
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):
for rec in self.data:
if rec['item'] is None:
rec['item'] = SpotItem(rec, self)
return self.data['item']
def pointsAt(self, pos):
@ -506,8 +731,8 @@ class ScatterPlotItem(GraphicsObject):
#else:
#print "No hit:", (x, y), (sx, sy)
#print " ", (sx-s2x, sy-s2y), (sx+s2x, sy+s2y)
pts.sort(lambda a,b: cmp(b.zValue(), a.zValue()))
return pts
#pts.sort(lambda a,b: cmp(b.zValue(), a.zValue()))
return pts[::-1]
def mouseClickEvent(self, ev):
@ -524,7 +749,7 @@ class ScatterPlotItem(GraphicsObject):
ev.ignore()
class SpotItem(GraphicsItem):
class SpotItem(object):
"""
Class referring to individual spots in a scatter plot.
These can be retrieved by calling ScatterPlotItem.points() or
@ -532,14 +757,12 @@ class SpotItem(GraphicsItem):
"""
def __init__(self, data, plot):
GraphicsItem.__init__(self, register=False)
#GraphicsItem.__init__(self, register=False)
self._data = data
self._plot = plot
#self._viewBox = None
#self._viewWidget = None
self.setParentItem(plot)
self.setPos(QtCore.QPointF(data['x'], data['y']))
self.updateItem()
#self.setParentItem(plot)
#self.setPos(QtCore.QPointF(data['x'], data['y']))
#self.updateItem()
def data(self):
"""Return the user data associated with this spot."""
@ -553,6 +776,12 @@ class SpotItem(GraphicsItem):
else:
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):
"""Set the size of this spot.
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"""
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):
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 symbolOpts == (None, None, -1, ''):
pixmap = self._plot.defaultSpotPixmap()
else:
pixmap = makeSymbolPixmap(size=self.size(), pen=self.pen(), brush=self.brush(), symbol=self.symbol())
self.setPixmap(pixmap)
### If all symbol options are default, use default pixmap
#if symbolOpts == (None, None, -1, ''):
#pixmap = self._plot.defaultSpotPixmap()
#else:
#pixmap = makeSymbolPixmap(size=self.size(), pen=self.pen(), brush=self.brush(), symbol=self.symbol())
#self.setPixmap(pixmap)
class PathSpotItem(SpotItem, QtGui.QGraphicsPathItem):
def __init__(self, data, plot):
QtGui.QGraphicsPathItem.__init__(self)
SpotItem.__init__(self, data, plot)
#class PathSpotItem(SpotItem, QtGui.QGraphicsPathItem):
#def __init__(self, data, plot):
#QtGui.QGraphicsPathItem.__init__(self)
#SpotItem.__init__(self, data, plot)
def updateItem(self):
QtGui.QGraphicsPathItem.setPath(self, Symbols[self.symbol()])
QtGui.QGraphicsPathItem.setPen(self, self.pen())
QtGui.QGraphicsPathItem.setBrush(self, self.brush())
size = self.size()
self.resetTransform()
self.scale(size, size)
#def updateItem(self):
#QtGui.QGraphicsPathItem.setPath(self, Symbols[self.symbol()])
#QtGui.QGraphicsPathItem.setPen(self, self.pen())
#QtGui.QGraphicsPathItem.setBrush(self, self.brush())
#size = self.size()
#self.resetTransform()
#self.scale(size, size)

View File

@ -48,6 +48,7 @@ class ViewBox(GraphicsWidget):
sigRangeChanged = QtCore.Signal(object, object)
#sigActionPositionChanged = QtCore.Signal(object)
sigStateChanged = QtCore.Signal(object)
sigTransformChanged = QtCore.Signal(object)
## mouse modes
PanMode = 3
@ -307,10 +308,6 @@ class ViewBox(GraphicsWidget):
print("make qrectf failed:", self.state['viewRange'])
raise
#def viewportTransform(self):
##return self.itemTransform(self.childGroup)[0]
#return self.childGroup.itemTransform(self)[0]
def targetRange(self):
return [x[:] for x in self.state['targetRange']] ## return copy
@ -554,10 +551,10 @@ class ViewBox(GraphicsWidget):
## Make corrections to range
xr = childRange[ax]
if xr is not None:
if self.state['autoPan'][0]:
if self.state['autoPan'][ax]:
x = sum(xr) * 0.5
#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.setRight(x+w2)
childRange[ax] = [x-w2, x+w2]
@ -1127,29 +1124,15 @@ class ViewBox(GraphicsWidget):
m = QtGui.QTransform()
## First center the viewport at 0
#self.childGroup.resetTransform()
#self.resetTransform()
#center = self.transform().inverted()[0].map(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())
#print " not inverted; translate", center.x(), -center.y()
## Now scale and translate properly
m.scale(scale[0], scale[1])
st = Point(vr.center())
#st = translate
m.translate(-st[0], -st[1])
self.childGroup.setTransform(m)
#self.setTransform(m)
#self.prepareGeometryChange()
#self.currentScale = scale
if changed[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]))
if any(changed):
self.sigRangeChanged.emit(self, self.state['viewRange'])
self.sigTransformChanged.emit(self)
def paint(self, p, opt, widget):
if self.border is not None:
@ -1165,20 +1150,6 @@ class ViewBox(GraphicsWidget):
#p.fillRect(bounds, QtGui.QColor(0, 0, 0))
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):
bg = self.state['background']
if bg is None:

View File

@ -21,13 +21,13 @@ def mkQApp():
class GraphicsWindow(GraphicsLayoutWidget):
def __init__(self, title=None, size=(800,600), **kargs):
mkQApp()
self.win = QtGui.QMainWindow()
#self.win = QtGui.QMainWindow()
GraphicsLayoutWidget.__init__(self, **kargs)
self.win.setCentralWidget(self)
self.win.resize(*size)
#self.win.setCentralWidget(self)
self.resize(*size)
if title is not None:
self.win.setWindowTitle(title)
self.win.show()
self.setWindowTitle(title)
self.show()
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):
"""Draws points at a list of 3D positions."""
def __init__(self, data=None):
def __init__(self, **kwds):
GLGraphicsItem.__init__(self)
self.data = []
if data is not None:
self.setData(data)
self.pos = []
self.size = 10
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
color (r,g,b,a) tuple of floats (0.0-1.0) or QColor
size (float) diameter of spot in pixels
pos (N,3) array of floats specifying point locations.
color (N,4) array of floats (0.0-1.0) specifying
spot colors OR a tuple of floats specifying
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.
==================== ==================================================
"""
self.data = data
args = ['pos', 'color', 'size', 'pxMode']
for k in kwds.keys():
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()
def initializeGL(self):
## Generate texture for rendering points
w = 64
def fn(x,y):
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_T, GL_CLAMP_TO_EDGE)
for pt in self.data:
pos = pt['pos']
try:
color = pt['color']
except KeyError:
color = (1,1,1,1)
try:
size = pt['size']
except KeyError:
size = 10
if isinstance(color, QtGui.QColor):
color = fn.glColor(color)
pxSize = self.view().pixelSize(QtGui.QVector3D(*pos))
if self.pxMode:
glVertexPointerf(self.pos)
if isinstance(self.color, np.ndarray):
glColorPointerf(self.color)
else:
if isinstance(self.color, QtGui.QColor):
glColor4f(*fn.glColor(self.color))
else:
glColor4f(*self.color)
glPointSize(size / pxSize)
glBegin( GL_POINTS )
glColor4f(*color) # x is blue
#glNormal3f(size, 0, 0)
glVertex3f(*pos)
glEnd()
if isinstance(self.size, np.ndarray):
raise Exception('Array size not yet supported in pxMode (hopefully soon)')
glPointSize(self.size)
glEnableClientState(GL_VERTEX_ARRAY)
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)."""
sigRangeChanged = QtCore.Signal(object, object)
sigTransformChanged = QtCore.Signal(object)
sigMouseReleased = QtCore.Signal(object)
sigSceneMouseMoved = QtCore.Signal(object)
#sigRegionChanged = QtCore.Signal(object)
@ -212,6 +213,7 @@ class GraphicsView(QtGui.QGraphicsView):
self.fitInView(self.range, QtCore.Qt.IgnoreAspectRatio)
self.sigRangeChanged.emit(self, self.range)
self.sigTransformChanged.emit(self)
if propagate:
for v in self.lockedViewports: