Merged changes from acq4

Added style options to flowchart connection lines
SVG export bug - correctly handle coordinate corrections for groups with mixed elements
Updates to ScatterPlotWidget, DataFilterWidget, and ColorMapWidget
Added exit() function for working around PyQt exit crashes
Bidirectional pseudoScatter for beeswarm plots
Added several examples
Added BarGraphItem
Fixed GraphItem antialiasing
Added parentChanged and viewChanged hooks to GraphicsItem
Made LabelItem a subclass of GraphicsWidgetAnchor
Documented planned features for PlotDataItem (these should be fixed before next release)
ScaleBar complete rewrite
Re-fixed crash bug in ScatterPlotItem
fixed scatterplotitem antialiasing
This commit is contained in:
Luke Campagnola 2013-03-26 15:42:07 -04:00
commit a67667b1ca
24 changed files with 676 additions and 139 deletions

View File

@ -97,6 +97,6 @@ Miscellaneous Functions
.. autofunction:: pyqtgraph.systemInfo .. autofunction:: pyqtgraph.systemInfo
.. autofunction:: pyqtgraph.exit

View File

@ -10,6 +10,9 @@ import pyqtgraph as pg
from pyqtgraph.Qt import QtCore, QtGui from pyqtgraph.Qt import QtCore, QtGui
import numpy as np import numpy as np
# Enable antialiasing for prettier plots
pg.setConfigOptions(antialias=True)
w = pg.GraphicsWindow() w = pg.GraphicsWindow()
w.setWindowTitle('pyqtgraph example: GraphItem') w.setWindowTitle('pyqtgraph example: GraphItem')
v = w.addViewBox() v = w.addViewBox()

View File

@ -21,7 +21,8 @@ win = pg.GraphicsWindow(title="Basic plotting examples")
win.resize(1000,600) win.resize(1000,600)
win.setWindowTitle('pyqtgraph example: Plotting') win.setWindowTitle('pyqtgraph example: Plotting')
# Enable antialiasing for prettier plots
pg.setConfigOptions(antialias=True)
p1 = win.addPlot(title="Basic array plotting", y=np.random.normal(size=100)) p1 = win.addPlot(title="Basic array plotting", y=np.random.normal(size=100))

31
examples/ScaleBar.py Normal file
View File

@ -0,0 +1,31 @@
# -*- coding: utf-8 -*-
"""
Demonstrates ScaleBar
"""
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
pg.mkQApp()
win = pg.GraphicsWindow()
win.setWindowTitle('pyqtgraph example: ScaleBar')
vb = win.addViewBox()
vb.setAspectLocked()
img = pg.ImageItem()
img.setImage(np.random.normal(size=(100,100)))
img.scale(0.01, 0.01)
vb.addItem(img)
scale = pg.ScaleBar(size=0.1)
scale.setParentItem(vb)
scale.anchor((1, 1), (1, 1), offset=(-20, -20))
## Start Qt event loop unless running in interactive mode or using pyside.
if __name__ == '__main__':
import sys
if (sys.flags.interactive != 1) or not hasattr(QtCore, 'PYQT_VERSION'):
QtGui.QApplication.instance().exec_()

View File

@ -0,0 +1,52 @@
# -*- 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
import numpy as np
pg.mkQApp()
spw = pg.ScatterPlotWidget()
spw.show()
data = np.array([
(1, 1, 3, 4, 'x'),
(2, 3, 3, 7, 'y'),
(3, 2, 5, 2, 'z'),
(4, 4, 6, 9, 'z'),
(5, 3, 6, 7, 'x'),
(6, 5, 4, 6, 'x'),
(7, 5, 8, 2, 'z'),
(8, 1, 2, 4, 'x'),
(9, 2, 3, 7, 'z'),
(0, 6, 0, 2, 'z'),
(1, 3, 1, 2, 'z'),
(2, 5, 4, 6, 'y'),
(3, 4, 8, 1, 'y'),
(4, 7, 6, 8, 'z'),
(5, 8, 7, 4, 'y'),
(6, 1, 2, 3, 'y'),
(7, 5, 3, 9, 'z'),
(8, 9, 3, 1, 'x'),
(9, 2, 6, 2, 'z'),
(0, 3, 4, 6, 'x'),
(1, 5, 9, 3, 'y'),
], dtype=[('col1', float), ('col2', float), ('col3', int), ('col4', int), ('col5', 'S10')])
spw.setFields([
('col1', {'units': 'm'}),
('col2', {'units': 'm'}),
('col3', {}),
('col4', {}),
('col5', {'mode': 'enum', 'values': ['x', 'y', 'z']}),
])
spw.setData(data)
## Start Qt event loop unless running in interactive mode or using pyside.
if __name__ == '__main__':
import sys
if (sys.flags.interactive != 1) or not hasattr(QtCore, 'PYQT_VERSION'):
QtGui.QApplication.instance().exec_()

View File

@ -3,9 +3,12 @@ import initExample ## Add path to library (just for examples; you do not need th
from pyqtgraph.Qt import QtGui, QtCore from pyqtgraph.Qt import QtGui, QtCore
import pyqtgraph as pg import pyqtgraph as pg
import numpy as np import numpy as np
pg.plot(np.random.normal(size=100000), title="Simplest possible plotting example") plt = pg.plot(np.random.normal(size=100), title="Simplest possible plotting example")
plt.getAxis('bottom').setTicks([[(x*20, str(x*20)) for x in range(6)]])
## Start Qt event loop unless running in interactive mode or using pyside. ## Start Qt event loop unless running in interactive mode or using pyside.
ex = pg.exporters.SVGExporter.SVGExporter(plt.plotItem.scene())
ex.export('/home/luke/tmp/test.svg')
if __name__ == '__main__': if __name__ == '__main__':
import sys import sys
if sys.flags.interactive != 1 or not hasattr(QtCore, 'PYQT_VERSION'): if sys.flags.interactive != 1 or not hasattr(QtCore, 'PYQT_VERSION'):

38
examples/beeswarm.py Normal file
View File

@ -0,0 +1,38 @@
# -*- coding: utf-8 -*-
"""
Example beeswarm / bar chart
"""
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.plot()
win.setWindowTitle('pyqtgraph example: beeswarm')
data = np.random.normal(size=(4,20))
data[0] += 5
data[1] += 7
data[2] += 5
data[3] = 10 + data[3] * 2
## Make bar graph
#bar = pg.BarGraphItem(x=range(4), height=data.mean(axis=1), width=0.5, brush=0.4)
#win.addItem(bar)
## add scatter plots on top
for i in range(4):
xvals = pg.pseudoScatter(data[i], spacing=0.4, bidir=True) * 0.2
win.plot(x=xvals+i, y=data[i], pen=None, symbol='o', symbolBrush=pg.intColor(i,6,maxValue=128))
## Make error bars
err = pg.ErrorBarItem(x=np.arange(4), y=data.mean(axis=1), height=data.std(axis=1), beam=0.5, pen={'color':'w', 'width':2})
win.addItem(err)
## Start Qt event loop unless running in interactive mode or using pyside.
if __name__ == '__main__':
import sys
if (sys.flags.interactive != 1) or not hasattr(QtCore, 'PYQT_VERSION'):
QtGui.QApplication.instance().exec_()

View File

@ -54,6 +54,7 @@ CONFIG_OPTIONS = {
'editorCommand': None, ## command used to invoke code editor from ConsoleWidgets 'editorCommand': None, ## command used to invoke code editor from ConsoleWidgets
'useWeave': True, ## Use weave to speed up some operations, if it is available 'useWeave': True, ## Use weave to speed up some operations, if it is available
'weaveDebug': False, ## Print full error message if weave compile fails 'weaveDebug': False, ## Print full error message if weave compile fails
'exitCleanup': True, ## Attempt to work around some exit crash bugs in PyQt and PySide
'enableExperimental': False, ## Enable experimental features (the curious can search for this key in the code) 'enableExperimental': False, ## Enable experimental features (the curious can search for this key in the code)
} }
@ -191,9 +192,20 @@ from .SignalProxy import *
from .colormap import * from .colormap import *
from .ptime import time from .ptime import time
##############################################################
## PyQt and PySide both are prone to crashing on exit.
## There are two general approaches to dealing with this:
## 1. Install atexit handlers that assist in tearing down to avoid crashes.
## This helps, but is never perfect.
## 2. Terminate the process before python starts tearing down
## This is potentially dangerous
## Attempts to work around exit crashes:
import atexit import atexit
def cleanup(): def cleanup():
if not getConfigOption('exitCleanup'):
return
ViewBox.quit() ## tell ViewBox that it doesn't need to deregister views anymore. ViewBox.quit() ## tell ViewBox that it doesn't need to deregister views anymore.
## Workaround for Qt exit crash: ## Workaround for Qt exit crash:
@ -213,6 +225,38 @@ def cleanup():
atexit.register(cleanup) atexit.register(cleanup)
## Optional function for exiting immediately (with some manual teardown)
def exit():
"""
Causes python to exit without garbage-collecting any objects, and thus avoids
calling object destructor methods. This is a sledgehammer workaround for
a variety of bugs in PyQt and Pyside that cause crashes on exit.
This function does the following in an attempt to 'safely' terminate
the process:
* Invoke atexit callbacks
* Close all open file handles
* os._exit()
Note: there is some potential for causing damage with this function if you
are using objects that _require_ their destructors to be called (for example,
to properly terminate log files, disconnect from devices, etc). Situations
like this are probably quite rare, but use at your own risk.
"""
## first disable our own cleanup function; won't be needing it.
setConfigOptions(exitCleanup=False)
## invoke atexit callbacks
atexit._run_exitfuncs()
## close file handles
os.closerange(3, 4096) ## just guessing on the maximum descriptor count..
os._exit(os.EX_OK)
## Convenience functions for command-line use ## Convenience functions for command-line use

View File

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

View File

@ -304,7 +304,36 @@ def _generateItemSvg(item, nodes=None, root=None):
def correctCoordinates(node, item): def correctCoordinates(node, item):
## Remove transformation matrices from <g> tags by applying matrix to coordinates inside. ## Remove transformation matrices from <g> tags by applying matrix to coordinates inside.
## Each item is represented by a single top-level group with one or more groups inside.
## Each inner group contains one or more drawing primitives, possibly of different types.
groups = node.getElementsByTagName('g') groups = node.getElementsByTagName('g')
## Since we leave text unchanged, groups which combine text and non-text primitives must be split apart.
## (if at some point we start correcting text transforms as well, then it should be safe to remove this)
groups2 = []
for grp in groups:
subGroups = [grp.cloneNode(deep=False)]
textGroup = None
for ch in grp.childNodes[:]:
if isinstance(ch, xml.Element):
if textGroup is None:
textGroup = ch.tagName == 'text'
if ch.tagName == 'text':
if textGroup is False:
subGroups.append(grp.cloneNode(deep=False))
textGroup = True
else:
if textGroup is True:
subGroups.append(grp.cloneNode(deep=False))
textGroup = False
subGroups[-1].appendChild(ch)
groups2.extend(subGroups)
for sg in subGroups:
node.insertBefore(sg, grp)
node.removeChild(grp)
groups = groups2
for grp in groups: for grp in groups:
matrix = grp.getAttribute('transform') matrix = grp.getAttribute('transform')
match = re.match(r'matrix\((.*)\)', matrix) match = re.match(r'matrix\((.*)\)', matrix)
@ -375,7 +404,6 @@ def correctCoordinates(node, item):
if removeTransform: if removeTransform:
grp.removeAttribute('transform') grp.removeAttribute('transform')
def itemTransform(item, root): def itemTransform(item, root):
## Return the transformation mapping item to root ## Return the transformation mapping item to root
## (actually to parent coordinate system of root) ## (actually to parent coordinate system of root)

View File

@ -523,6 +523,15 @@ class ConnectionItem(GraphicsObject):
self.hovered = False self.hovered = False
self.path = None self.path = None
self.shapePath = None self.shapePath = None
self.style = {
'shape': 'line',
'color': (100, 100, 250),
'width': 1.0,
'hoverColor': (150, 150, 250),
'hoverWidth': 1.0,
'selectedColor': (200, 200, 0),
'selectedWidth': 3.0,
}
#self.line = QtGui.QGraphicsLineItem(self) #self.line = QtGui.QGraphicsLineItem(self)
self.source.getViewBox().addItem(self) self.source.getViewBox().addItem(self)
self.updateLine() self.updateLine()
@ -537,6 +546,13 @@ class ConnectionItem(GraphicsObject):
self.target = target self.target = target
self.updateLine() self.updateLine()
def setStyle(self, **kwds):
self.style.update(kwds)
if 'shape' in kwds:
self.updateLine()
else:
self.update()
def updateLine(self): def updateLine(self):
start = Point(self.source.connectPoint()) start = Point(self.source.connectPoint())
if isinstance(self.target, TerminalGraphicsItem): if isinstance(self.target, TerminalGraphicsItem):
@ -547,19 +563,20 @@ class ConnectionItem(GraphicsObject):
return return
self.prepareGeometryChange() self.prepareGeometryChange()
self.path = QtGui.QPainterPath() self.path = self.generatePath(start, stop)
self.path.moveTo(start)
self.path.cubicTo(Point(stop.x(), start.y()), Point(start.x(), stop.y()), Point(stop.x(), stop.y()))
self.shapePath = None self.shapePath = None
#self.resetTransform()
#ang = (stop-start).angle(Point(0, 1))
#if ang is None:
#ang = 0
#self.rotate(ang)
#self.setPos(start)
#self.length = (start-stop).length()
self.update() self.update()
#self.line.setLine(start.x(), start.y(), stop.x(), stop.y())
def generatePath(self, start, stop):
path = QtGui.QPainterPath()
path.moveTo(start)
if self.style['shape'] == 'line':
path.lineTo(stop)
elif self.style['shape'] == 'cubic':
path.cubicTo(Point(stop.x(), start.y()), Point(start.x(), stop.y()), Point(stop.x(), stop.y()))
else:
raise Exception('Invalid shape "%s"; options are "line" or "cubic"' % self.style['shape'])
return path
def keyPressEvent(self, ev): def keyPressEvent(self, ev):
if ev.key() == QtCore.Qt.Key_Delete or ev.key() == QtCore.Qt.Key_Backspace: if ev.key() == QtCore.Qt.Key_Delete or ev.key() == QtCore.Qt.Key_Backspace:
@ -609,12 +626,12 @@ class ConnectionItem(GraphicsObject):
def paint(self, p, *args): def paint(self, p, *args):
if self.isSelected(): if self.isSelected():
p.setPen(fn.mkPen(200, 200, 0, width=3)) p.setPen(fn.mkPen(self.style['selectedColor'], width=self.style['selectedWidth']))
else: else:
if self.hovered: if self.hovered:
p.setPen(fn.mkPen(150, 150, 250, width=1)) p.setPen(fn.mkPen(self.style['hoverColor'], width=self.style['hoverWidth']))
else: else:
p.setPen(fn.mkPen(100, 100, 250, width=1)) p.setPen(fn.mkPen(self.style['color'], width=self.style['width']))
#p.drawLine(0, 0, 0, self.length) #p.drawLine(0, 0, 0, self.length)

View File

@ -1930,9 +1930,9 @@ def invertQTransform(tr):
return QtGui.QTransform(inv[0,0], inv[0,1], inv[0,2], inv[1,0], inv[1,1], inv[1,2], inv[2,0], inv[2,1]) return QtGui.QTransform(inv[0,0], inv[0,1], inv[0,2], inv[1,0], inv[1,1], inv[1,2], inv[2,0], inv[2,1])
def pseudoScatter(data, spacing=None, shuffle=True): def pseudoScatter(data, spacing=None, shuffle=True, bidir=False):
""" """
Used for examining the distribution of values in a set. Used for examining the distribution of values in a set. Produces scattering as in beeswarm or column scatter plots.
Given a list of x-values, construct a set of y-values such that an x,y scatter-plot 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). will not have overlapping points (it will look similar to a histogram).
@ -1959,23 +1959,41 @@ def pseudoScatter(data, spacing=None, shuffle=True):
xmask = dx < s2 # exclude anything too far away xmask = dx < s2 # exclude anything too far away
if xmask.sum() > 0: if xmask.sum() > 0:
dx = dx[xmask] if bidir:
dy = (s2 - dx)**0.5 dirs = [-1, 1]
else:
dirs = [1]
yopts = []
for direction in dirs:
y = 0
dx2 = dx[xmask]
dy = (s2 - dx2)**0.5
limits = np.empty((2,len(dy))) # ranges of y-values to exclude limits = np.empty((2,len(dy))) # ranges of y-values to exclude
limits[0] = y0[xmask] - dy limits[0] = y0[xmask] - dy
limits[1] = y0[xmask] + dy limits[1] = y0[xmask] + dy
while True: while True:
# ignore anything below this y-value # ignore anything below this y-value
if direction > 0:
mask = limits[1] >= y mask = limits[1] >= y
limits = limits[:,mask] else:
mask = limits[0] <= y
limits2 = limits[:,mask]
# are we inside an excluded region? # are we inside an excluded region?
mask = (limits[0] < y) & (limits[1] > y) mask = (limits2[0] < y) & (limits2[1] > y)
if mask.sum() == 0: if mask.sum() == 0:
break break
y = limits[:,mask].max()
if direction > 0:
y = limits2[:,mask].max()
else:
y = limits2[:,mask].min()
yopts.append(y)
if bidir:
y = yopts[0] if -yopts[0] < yopts[1] else yopts[1]
else:
y = yopts[0]
yvals[i] = y yvals[i] = y
return yvals[np.argsort(inds)] ## un-shuffle values before returning return yvals[np.argsort(inds)] ## un-shuffle values before returning

View File

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

View File

@ -103,6 +103,8 @@ class GraphItem(GraphicsObject):
def paint(self, p, *args): def paint(self, p, *args):
if self.picture == None: if self.picture == None:
self.generatePicture() self.generatePicture()
if pg.getConfigOption('antialias') is True:
p.setRenderHint(p.Antialiasing)
self.picture.play(p) self.picture.play(p)
def boundingRect(self): def boundingRect(self):

View File

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

View File

@ -19,7 +19,7 @@ class GraphicsObject(GraphicsItem, QtGui.QGraphicsObject):
def itemChange(self, change, value): def itemChange(self, change, value):
ret = QtGui.QGraphicsObject.itemChange(self, change, value) ret = QtGui.QGraphicsObject.itemChange(self, change, value)
if change in [self.ItemParentHasChanged, self.ItemSceneHasChanged]: if change in [self.ItemParentHasChanged, self.ItemSceneHasChanged]:
self._updateView() self.parentChanged()
if change in [self.ItemPositionHasChanged, self.ItemTransformHasChanged]: if change in [self.ItemPositionHasChanged, self.ItemTransformHasChanged]:
self.informViewBoundsChanged() self.informViewBoundsChanged()

View File

@ -5,7 +5,9 @@ from ..Point import Point
class GraphicsWidgetAnchor(object): class GraphicsWidgetAnchor(object):
""" """
Class used to allow GraphicsWidgets to anchor to a specific position on their Class used to allow GraphicsWidgets to anchor to a specific position on their
parent. parent. The item will be automatically repositioned if the parent is resized.
This is used, for example, to anchor a LegendItem to a corner of its parent
PlotItem.
""" """

View File

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

View File

@ -84,13 +84,24 @@ class PlotDataItem(GraphicsObject):
**Optimization keyword arguments:** **Optimization keyword arguments:**
========== ===================================================================== ============ =====================================================================
antialias (bool) By default, antialiasing is disabled to improve performance. antialias (bool) By default, antialiasing is disabled to improve performance.
Note that in some cases (in particluar, when pxMode=True), points Note that in some cases (in particluar, when pxMode=True), points
will be rendered antialiased even if this is set to False. will be rendered antialiased even if this is set to False.
decimate (int) Sub-sample data by selecting every nth sample before plotting
onlyVisible (bool) If True, only plot data that is visible within the X range of
the containing ViewBox. This can improve performance when plotting
very large data sets where only a fraction of the data is visible
at any time.
autoResample (bool) If True, resample the data before plotting to avoid plotting
multiple line segments per pixel. This can improve performance when
viewing very high-density data, but increases the initial overhead
and memory usage.
sampleRate (float) The sample rate of the data along the X axis (for data with
a fixed sample rate). Providing this value improves performance of
the *onlyVisible* and *autoResample* options.
identical *deprecated* identical *deprecated*
decimate (int) sub-sample data by selecting every nth sample before plotting ============ =====================================================================
========== =====================================================================
**Meta-info keyword arguments:** **Meta-info keyword arguments:**

View File

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

View File

@ -677,15 +677,12 @@ class ScatterPlotItem(GraphicsObject):
pts[1] = self.data['y'] pts[1] = self.data['y']
pts = fn.transformCoordinates(tr, pts) pts = fn.transformCoordinates(tr, pts)
self.fragments = [] self.fragments = []
pts = np.clip(pts, -2**31, 2**31) ## prevent Qt segmentation fault. pts = np.clip(pts, -2**30, 2**30) ## prevent Qt segmentation fault.
## Still won't be able to render correctly, though. ## Still won't be able to render correctly, though.
for i in xrange(len(self.data)): for i in xrange(len(self.data)):
rec = self.data[i] rec = self.data[i]
pos = QtCore.QPointF(pts[0,i], pts[1,i]) pos = QtCore.QPointF(pts[0,i], pts[1,i])
x,y,w,h = rec['fragCoords'] x,y,w,h = rec['fragCoords']
if abs(w) > 10000 or abs(h) > 10000:
print self.data
raise Exception("fragment corrupt")
rect = QtCore.QRectF(y, x, h, w) rect = QtCore.QRectF(y, x, h, w)
self.fragments.append(QtGui.QPainter.PixmapFragment.create(pos, rect)) self.fragments.append(QtGui.QPainter.PixmapFragment.create(pos, rect))
@ -743,6 +740,7 @@ class ScatterPlotItem(GraphicsObject):
drawSymbol(p2, *self.getSpotOpts(rec, scale)) drawSymbol(p2, *self.getSpotOpts(rec, scale))
p2.end() p2.end()
p.setRenderHint(p.Antialiasing, aa)
self.picture.play(p) self.picture.play(p)
def points(self): def points(self):

View File

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

View File

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

View File

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