merged many changes from acq4

This commit is contained in:
Luke Campagnola 2013-03-26 13:46:26 -04:00
parent f029e7893e
commit 8828892e55
21 changed files with 522 additions and 123 deletions

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

@ -16,8 +16,22 @@ data = np.array([
(3, 2, 5, 2, 'z'), (3, 2, 5, 2, 'z'),
(4, 4, 6, 9, 'z'), (4, 4, 6, 9, 'z'),
(5, 3, 6, 7, 'x'), (5, 3, 6, 7, 'x'),
(6, 5, 2, 6, 'y'), (6, 5, 4, 6, 'x'),
(7, 5, 7, 2, 'z'), (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')]) ], dtype=[('col1', float), ('col2', float), ('col3', int), ('col4', int), ('col5', 'S10')])
spw.setFields([ spw.setFields([

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

@ -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)
@ -374,7 +403,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

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]
limits = np.empty((2,len(dy))) # ranges of y-values to exclude else:
limits[0] = y0[xmask] - dy dirs = [1]
limits[1] = y0[xmask] + dy yopts = []
for direction in dirs:
while True: y = 0
# ignore anything below this y-value dx2 = dx[xmask]
mask = limits[1] >= y dy = (s2 - dx2)**0.5
limits = limits[:,mask] limits = np.empty((2,len(dy))) # ranges of y-values to exclude
limits[0] = y0[xmask] - dy
# are we inside an excluded region? limits[1] = y0[xmask] + dy
mask = (limits[0] < y) & (limits[1] > y) while True:
if mask.sum() == 0: # ignore anything below this y-value
break if direction > 0:
y = limits[:,mask].max() mask = limits[1] >= y
else:
mask = limits[0] <= y
limits2 = limits[:,mask]
# are we inside an excluded region?
mask = (limits2[0] < y) & (limits2[1] > y)
if mask.sum() == 0:
break
if direction > 0:
y = limits2[:,mask].max()
else:
y = limits2[:,mask].min()
yopts.append(y)
if bidir:
y = yopts[0] if -yopts[0] < yopts[1] else yopts[1]
else:
y = yopts[0]
yvals[i] = y 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

@ -402,7 +402,6 @@ class PlotCurveItem(GraphicsObject):
aa = self.opts['antialias'] aa = self.opts['antialias']
p.setRenderHint(p.Antialiasing, aa) p.setRenderHint(p.Antialiasing, aa)
if self.opts['brush'] is not None and self.opts['fillLevel'] is not None: if self.opts['brush'] is not None and self.opts['fillLevel'] is not None:
if self.fillPath is None: if self.fillPath is None:

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 def parentChanged(self):
x = rect.right() + (rect.left()-rect.right()) * 0.02 view = self.parentItem()
x1 = x - self.size if view is None:
return
view.sigRangeChanged.connect(self.updateBar)
self.updateBar()
p.setPen(self.pen)
p.setBrush(self.brush)
rect = QtCore.QRectF(
QtCore.QPointF(x1, y1),
QtCore.QPointF(x, y)
)
p.translate(x1, y1)
p.scale(rect.width(), rect.height())
p.drawRect(0, 0, 1, 1)
alpha = np.clip(((self.size/unit[0]) - 40.) * 255. / 80., 0, 255) def updateBar(self):
p.setPen(QtGui.QPen(QtGui.QColor(0, 0, 0, alpha))) view = self.parentItem()
for i in range(1, 10): if view is None:
#x2 = x + (x1-x) * 0.1 * i return
x2 = 0.1 * i p1 = view.mapFromViewToItem(self, QtCore.QPointF(0,0))
p.drawLine(QtCore.QPointF(x2, 0), QtCore.QPointF(x2, 1)) 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): #def setSize(self, s):
self.size = s #self.size = s

View File

@ -740,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

@ -524,12 +524,13 @@ class ViewBox(GraphicsWidget):
if t is not None: if t is not None:
t = Point(t) t = Point(t)
self.setRange(vr.translated(t), padding=0) self.setRange(vr.translated(t), padding=0)
elif x is not None: else:
x1, x2 = vr.left()+x, vr.right()+x if x is not None:
self.setXRange(x1, x2, padding=0) x1, x2 = vr.left()+x, vr.right()+x
elif y is not None: self.setXRange(x1, x2, padding=0)
y1, y2 = vr.top()+y, vr.bottom()+y if y is not None:
self.setYRange(y1, y2, padding=0) y1, y2 = vr.top()+y, vr.bottom()+y
self.setYRange(y1, y2, padding=0)
@ -1090,10 +1091,10 @@ class ViewBox(GraphicsWidget):
xr = item.dataBounds(0, frac=frac[0], orthoRange=orthoRange[0]) xr = item.dataBounds(0, frac=frac[0], orthoRange=orthoRange[0])
yr = item.dataBounds(1, frac=frac[1], orthoRange=orthoRange[1]) yr = item.dataBounds(1, frac=frac[1], orthoRange=orthoRange[1])
pxPad = 0 if not hasattr(item, 'pixelPadding') else item.pixelPadding() pxPad = 0 if not hasattr(item, 'pixelPadding') else item.pixelPadding()
if xr is None or (xr[0] is None and xr[1] is None) or np.isnan(xr).any() or np.isinf(xr).any(): if xr is None or xr == (None, None) or np.isnan(xr).any() or np.isinf(xr).any():
useX = False useX = False
xr = (0,0) xr = (0,0)
if yr is None or (yr[0] is None and yr[1] is None) or np.isnan(yr).any() or np.isinf(yr).any(): if yr is None or yr == (None, None) or np.isnan(yr).any() or np.isinf(yr).any():
useY = False useY = False
yr = (0,0) yr = (0,0)

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,12 +169,14 @@ 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 = [] childs = []
for v in vals: for val,vname in vals.items():
ch = ptree.Parameter.create(name=str(v), type='color') ch = ptree.Parameter.create(name=vname, type='color')
ch.maskValue = v ch.maskValue = val
childs.append(ch) childs.append(ch)
ptree.types.GroupParameter.__init__(self, ptree.types.GroupParameter.__init__(self,

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,18 +72,28 @@ 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]
#mask &= (vals >= mn) #mask &= (vals >= mn)
#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,19 +102,24 @@ 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 >= self['Min']) & (vals < self['Max']) ## 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 = [] childs = []
for v in vals: if isinstance(vals, list):
ch = ptree.Parameter.create(name=str(v), type='bool', value=True) vals = OrderedDict([(v,str(v)) for v in vals])
ch.maskValue = v for val,vname in vals.items():
ch = ptree.Parameter.create(name=vname, type='bool', value=True)
ch.maskValue = val
childs.append(ch) childs.append(ch)
ch = ptree.Parameter.create(name='(other)', type='bool', value=True) ch = ptree.Parameter.create(name='(other)', type='bool', value=True)
ch.maskValue = '__other__' ch.maskValue = '__other__'
@ -112,10 +129,10 @@ class EnumFilterItem(ptree.types.SimpleParameter):
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(data), dtype=bool) otherMask = np.ones(len(vals), dtype=bool)
for c in self: for c in self:
key = c.maskValue key = c.maskValue
if key == '__other__': if key == '__other__':
@ -125,4 +142,9 @@ class EnumFilterItem(ptree.types.SimpleParameter):
otherMask &= m otherMask &= m
if c.value() is False: if c.value() is False:
mask &= m mask &= m
return mask 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,6 +48,12 @@ 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.mouseOverField = None
self.scatterPlot = None self.scatterPlot = None
@ -97,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()
@ -125,69 +139,69 @@ 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 len(vals) 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: if self.scatterPlot is not None:
try: try:
self.scatterPlot.sigPointsClicked.disconnect(self.plotClicked) self.scatterPlot.sigPointsClicked.disconnect(self.plotClicked)
except: except:
pass pass
self.scatterPlot = self.plot.plot(x, y, data=data[mask], **style) self.scatterPlot = self.plot.plot(xy[0], xy[1], data=data[mask], **style)
self.scatterPlot.sigPointsClicked.connect(self.plotClicked) self.scatterPlot.sigPointsClicked.connect(self.plotClicked)