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
import numpy as np
# Enable antialiasing for prettier plots
pg.setConfigOptions(antialias=True)
w = pg.GraphicsWindow()
w.setWindowTitle('pyqtgraph example: GraphItem')
v = w.addViewBox()

View File

@ -21,7 +21,8 @@ win = pg.GraphicsWindow(title="Basic plotting examples")
win.resize(1000,600)
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))

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'),
(4, 4, 6, 9, 'z'),
(5, 3, 6, 7, 'x'),
(6, 5, 2, 6, 'y'),
(7, 5, 7, 2, 'z'),
(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([

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
import pyqtgraph as pg
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.
ex = pg.exporters.SVGExporter.SVGExporter(plt.plotItem.scene())
ex.export('/home/luke/tmp/test.svg')
if __name__ == '__main__':
import sys
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):
## Remove transformation matrices from <g> tags by applying matrix to coordinates inside.
## Each item is represented by a single top-level group with one or more groups inside.
## Each inner group contains one or more drawing primitives, possibly of different types.
groups = node.getElementsByTagName('g')
## Since we leave text unchanged, groups which combine text and non-text primitives must be split apart.
## (if at some point we start correcting text transforms as well, then it should be safe to remove this)
groups2 = []
for grp in groups:
subGroups = [grp.cloneNode(deep=False)]
textGroup = None
for ch in grp.childNodes[:]:
if isinstance(ch, xml.Element):
if textGroup is None:
textGroup = ch.tagName == 'text'
if ch.tagName == 'text':
if textGroup is False:
subGroups.append(grp.cloneNode(deep=False))
textGroup = True
else:
if textGroup is True:
subGroups.append(grp.cloneNode(deep=False))
textGroup = False
subGroups[-1].appendChild(ch)
groups2.extend(subGroups)
for sg in subGroups:
node.insertBefore(sg, grp)
node.removeChild(grp)
groups = groups2
for grp in groups:
matrix = grp.getAttribute('transform')
match = re.match(r'matrix\((.*)\)', matrix)
@ -374,7 +403,6 @@ def correctCoordinates(node, item):
if removeTransform:
grp.removeAttribute('transform')
def itemTransform(item, 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])
def pseudoScatter(data, spacing=None, shuffle=True):
def pseudoScatter(data, spacing=None, shuffle=True, bidir=False):
"""
Used for examining the distribution of values in a set.
Used for examining the distribution of values in a set. Produces scattering as in beeswarm or column scatter plots.
Given a list of x-values, construct a set of y-values such that an x,y scatter-plot
will not have overlapping points (it will look similar to a histogram).
@ -1959,23 +1959,41 @@ def pseudoScatter(data, spacing=None, shuffle=True):
xmask = dx < s2 # exclude anything too far away
if xmask.sum() > 0:
dx = dx[xmask]
dy = (s2 - dx)**0.5
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()
if bidir:
dirs = [-1, 1]
else:
dirs = [1]
yopts = []
for direction in dirs:
y = 0
dx2 = dx[xmask]
dy = (s2 - dx2)**0.5
limits = np.empty((2,len(dy))) # ranges of y-values to exclude
limits[0] = y0[xmask] - dy
limits[1] = y0[xmask] + dy
while True:
# ignore anything below this y-value
if direction > 0:
mask = limits[1] >= y
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
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):
if self.picture == None:
self.generatePicture()
if pg.getConfigOption('antialias') is True:
p.setRenderHint(p.Antialiasing)
self.picture.play(p)
def boundingRect(self):

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -740,6 +740,7 @@ class ScatterPlotItem(GraphicsObject):
drawSymbol(p2, *self.getSpotOpts(rec, scale))
p2.end()
p.setRenderHint(p.Antialiasing, aa)
self.picture.play(p)
def points(self):

View File

@ -524,12 +524,13 @@ class ViewBox(GraphicsWidget):
if t is not None:
t = Point(t)
self.setRange(vr.translated(t), padding=0)
elif x is not None:
x1, x2 = vr.left()+x, vr.right()+x
self.setXRange(x1, x2, padding=0)
elif y is not None:
y1, y2 = vr.top()+y, vr.bottom()+y
self.setYRange(y1, y2, padding=0)
else:
if x is not None:
x1, x2 = vr.left()+x, vr.right()+x
self.setXRange(x1, x2, padding=0)
if y is not None:
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])
yr = item.dataBounds(1, frac=frac[1], orthoRange=orthoRange[1])
pxPad = 0 if not hasattr(item, 'pixelPadding') else item.pixelPadding()
if xr is None or (xr[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
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
yr = (0,0)

View File

@ -72,7 +72,8 @@ class ColorMapParameter(ptree.types.GroupParameter):
(see *values* option).
units String indicating the units of the data for this field.
values List of unique values for which the user may assign a
color when mode=='enum'.
color when mode=='enum'. Optionally may specify a dict
instead {value: name}.
============== ============================================================
"""
self.fields = OrderedDict(fields)
@ -168,12 +169,14 @@ class EnumColorMapItem(ptree.types.GroupParameter):
def __init__(self, name, opts):
self.fieldName = name
vals = opts.get('values', [])
if isinstance(vals, list):
vals = OrderedDict([(v,str(v)) for v in vals])
childs = [{'name': v, 'type': 'color'} for v in vals]
childs = []
for v in vals:
ch = ptree.Parameter.create(name=str(v), type='color')
ch.maskValue = v
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,

View File

@ -2,6 +2,7 @@ from pyqtgraph.Qt import QtGui, QtCore
import pyqtgraph.parametertree as ptree
import numpy as np
from pyqtgraph.pgcollections import OrderedDict
import pyqtgraph as pg
__all__ = ['DataFilterWidget']
@ -22,6 +23,7 @@ class DataFilterWidget(ptree.ParameterTree):
self.setFields = self.params.setFields
self.filterData = self.params.filterData
self.describe = self.params.describe
def filterChanged(self):
self.sigFilterChanged.emit(self)
@ -70,18 +72,28 @@ class DataFilterParameter(ptree.types.GroupParameter):
for fp in self:
if fp.value() is False:
continue
mask &= fp.generateMask(data)
mask &= fp.generateMask(data, mask.copy())
#key, mn, mx = fp.fieldName, fp['Min'], fp['Max']
#vals = data[key]
#mask &= (vals >= mn)
#mask &= (vals < mx) ## Use inclusive minimum and non-inclusive maximum. This makes it easier to create non-overlapping selections
return mask
def describe(self):
"""Return a list of strings describing the currently enabled filters."""
desc = []
for fp in self:
if fp.value() is False:
continue
desc.append(fp.describe())
return desc
class RangeFilterItem(ptree.types.SimpleParameter):
def __init__(self, name, opts):
self.fieldName = name
units = opts.get('units', '')
self.units = units
ptree.types.SimpleParameter.__init__(self,
name=name, autoIncrementName=True, type='bool', value=True, removable=True, renamable=True,
children=[
@ -90,19 +102,24 @@ class RangeFilterItem(ptree.types.SimpleParameter):
dict(name='Max', type='float', value=1.0, suffix=units, siPrefix=True),
])
def generateMask(self, data):
vals = data[self.fieldName]
return (vals >= self['Min']) & (vals < self['Max']) ## Use inclusive minimum and non-inclusive maximum. This makes it easier to create non-overlapping selections
def generateMask(self, data, mask):
vals = data[self.fieldName][mask]
mask[mask] = (vals >= self['Min']) & (vals < self['Max']) ## Use inclusive minimum and non-inclusive maximum. This makes it easier to create non-overlapping selections
return mask
def describe(self):
return "%s < %s < %s" % (pg.siFormat(self['Min'], suffix=self.units), self.fieldName, pg.siFormat(self['Max'], suffix=self.units))
class EnumFilterItem(ptree.types.SimpleParameter):
def __init__(self, name, opts):
self.fieldName = name
vals = opts.get('values', [])
childs = []
for v in vals:
ch = ptree.Parameter.create(name=str(v), type='bool', value=True)
ch.maskValue = v
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__'
@ -112,10 +129,10 @@ class EnumFilterItem(ptree.types.SimpleParameter):
name=name, autoIncrementName=True, type='bool', value=True, removable=True, renamable=True,
children=childs)
def generateMask(self, data):
vals = data[self.fieldName]
mask = np.ones(len(data), dtype=bool)
otherMask = np.ones(len(data), dtype=bool)
def generateMask(self, data, startMask):
vals = data[self.fieldName][startMask]
mask = np.ones(len(vals), dtype=bool)
otherMask = np.ones(len(vals), dtype=bool)
for c in self:
key = c.maskValue
if key == '__other__':
@ -125,4 +142,9 @@ class EnumFilterItem(ptree.types.SimpleParameter):
otherMask &= m
if c.value() is False:
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 numpy as np
from pyqtgraph.pgcollections import OrderedDict
import pyqtgraph as pg
__all__ = ['ScatterPlotWidget']
@ -47,6 +48,12 @@ class ScatterPlotWidget(QtGui.QSplitter):
self.ctrlPanel.addWidget(self.ptree)
self.addWidget(self.plot)
bg = pg.mkColor(pg.getConfigOption('background'))
bg.setAlpha(150)
self.filterText = pg.TextItem(border=pg.getConfigOption('foreground'), color=bg)
self.filterText.setPos(60,20)
self.filterText.setParentItem(self.plot.plotItem)
self.data = None
self.mouseOverField = None
self.scatterPlot = None
@ -97,6 +104,13 @@ class ScatterPlotWidget(QtGui.QSplitter):
def filterChanged(self, f):
self.filtered = None
self.updatePlot()
desc = self.filter.describe()
if len(desc) == 0:
self.filterText.setVisible(False)
else:
self.filterText.setText('\n'.join(desc))
self.filterText.setVisible(True)
def updatePlot(self):
self.plot.clear()
@ -125,69 +139,69 @@ class ScatterPlotWidget(QtGui.QSplitter):
self.plot.setLabels(left=('N', ''), bottom=(sel[0], units[0]), title='')
if len(data) == 0:
return
x = data[sel[0]]
#if x.dtype.kind == 'f':
#mask = ~np.isnan(x)
#else:
#mask = np.ones(len(x), dtype=bool)
#x = x[mask]
#style['symbolBrush'] = colors[mask]
y = None
#x = data[sel[0]]
#y = None
xy = [data[sel[0]], None]
elif len(sel) == 2:
self.plot.setLabels(left=(sel[1],units[1]), bottom=(sel[0],units[0]))
if len(data) == 0:
return
xydata = []
for ax in [0,1]:
d = data[sel[ax]]
## scatter catecorical values just a bit so they show up better in the scatter plot.
#if sel[ax] in ['MorphologyBSMean', 'MorphologyTDMean', 'FIType']:
#d += np.random.normal(size=len(cells), scale=0.1)
xydata.append(d)
x,y = xydata
#mask = np.ones(len(x), dtype=bool)
#if x.dtype.kind == 'f':
#mask |= ~np.isnan(x)
#if y.dtype.kind == 'f':
#mask |= ~np.isnan(y)
#x = x[mask]
#y = y[mask]
#style['symbolBrush'] = colors[mask]
xy = [data[sel[0]], data[sel[1]]]
#xydata = []
#for ax in [0,1]:
#d = data[sel[ax]]
### scatter catecorical values just a bit so they show up better in the scatter plot.
##if sel[ax] in ['MorphologyBSMean', 'MorphologyTDMean', 'FIType']:
##d += np.random.normal(size=len(cells), scale=0.1)
#xydata.append(d)
#x,y = xydata
## convert enum-type fields to float, set axis labels
xy = [x,y]
enum = [False, False]
for i in [0,1]:
axis = self.plot.getAxis(['bottom', 'left'][i])
if xy[i] is not None and xy[i].dtype.kind in ('S', 'O'):
if xy[i] is not None and (self.fields[sel[i]].get('mode', None) == 'enum' or xy[i].dtype.kind in ('S', 'O')):
vals = self.fields[sel[i]].get('values', list(set(xy[i])))
xy[i] = np.array([vals.index(x) if x in vals else len(vals) for x in xy[i]], dtype=float)
axis.setTicks([list(enumerate(vals))])
enum[i] = True
else:
axis.setTicks(None) # reset to automatic ticking
x,y = xy
## mask out any nan values
mask = np.ones(len(x), dtype=bool)
if x.dtype.kind == 'f':
mask &= ~np.isnan(x)
if y is not None and y.dtype.kind == 'f':
mask &= ~np.isnan(y)
x = x[mask]
mask = np.ones(len(xy[0]), dtype=bool)
if xy[0].dtype.kind == 'f':
mask &= ~np.isnan(xy[0])
if xy[1] is not None and xy[1].dtype.kind == 'f':
mask &= ~np.isnan(xy[1])
xy[0] = xy[0][mask]
style['symbolBrush'] = colors[mask]
## Scatter y-values for a histogram-like appearance
if y is None:
y = fn.pseudoScatter(x)
if xy[1] is None:
## column scatter plot
xy[1] = fn.pseudoScatter(xy[0])
else:
y = y[mask]
## beeswarm plots
xy[1] = xy[1][mask]
for ax in [0,1]:
if not enum[ax]:
continue
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(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)