pyqtgraph/graphicsItems/ScatterPlotItem.py

378 lines
13 KiB
Python
Raw Normal View History

from pyqtgraph.Qt import QtGui, QtCore
from pyqtgraph.Point import Point
import pyqtgraph.functions as fn
from GraphicsObject import GraphicsObject
__all__ = ['ScatterPlotItem', 'SpotItem']
class ScatterPlotItem(GraphicsObject):
#sigPointClicked = QtCore.Signal(object, object)
sigClicked = QtCore.Signal(object, object) ## self, points
sigPlotChanged = QtCore.Signal(object)
def __init__(self, spots=None, x=None, y=None, pxMode=True, pen='default', brush='default', size=7,
symbol=None, identical=False, data=None):
"""
Arguments:
spots: list of dicts. Each dict specifies parameters for a single spot:
{'pos': (x,y), 'size', 'pen', 'brush', 'symbol'}
x,y: array of x,y values. Alternatively, specify spots['pos'] = (x,y)
pxMode: If True, spots are always the same size regardless of scaling, and size is given in px.
Otherwise, size is in scene coordinates and the spots scale with the view.
identical: If True, all spots are forced to look identical.
This can result in performance enhancement.
symbol can be one of:
'o' circle
's' square
't' triangle
'd' diamond
'+' plus
"""
GraphicsObject.__init__(self)
self.spots = []
self.range = [[0,0], [0,0]]
self.identical = identical
self._spotPixmap = None
if brush == 'default':
self.brush = QtGui.QBrush(QtGui.QColor(100, 100, 150))
else:
self.brush = fn.mkBrush(brush)
if pen == 'default':
self.pen = QtGui.QPen(QtGui.QColor(200, 200, 200))
else:
self.pen = fn.mkPen(pen)
self.symbol = symbol
self.size = size
self.pxMode = pxMode
if spots is not None or x is not None:
self.setPoints(spots, x, y, data)
#self.optimize = optimize
#if optimize:
#self.spotImage = QtGui.QImage(size, size, QtGui.QImage.Format_ARGB32_Premultiplied)
#self.spotImage.fill(0)
#p = QtGui.QPainter(self.spotImage)
#p.setRenderHint(p.Antialiasing)
#p.setBrush(brush)
#p.setPen(pen)
#p.drawEllipse(0, 0, size, size)
#p.end()
#self.optimizePixmap = QtGui.QPixmap(self.spotImage)
#self.optimizeFragments = []
#self.setFlags(self.flags() | self.ItemIgnoresTransformations)
def implements(self, interface=None):
ints = ['plotData']
if interface is None:
return ints
return interface in ints
def setPxMode(self, mode):
self.pxMode = mode
def clear(self):
for i in self.spots:
i.setParentItem(None)
s = i.scene()
if s is not None:
s.removeItem(i)
self.spots = []
def getRange(self, ax, percent):
return self.range[ax]
def setPoints(self, spots=None, x=None, y=None, data=None):
"""
Remove all existing points in the scatter plot and add a new set.
Arguments:
spots - list of dicts specifying parameters for each spot
[ {'pos': (x,y), 'pen': 'r', ...}, ...]
x, y - arrays specifying location of spots to add.
all other parameters (pen, symbol, etc.) will be set to the default
values for this scatter plot.
these arguments are IGNORED if 'spots' is specified
data - list of arbitrary objects to be assigned to spot.data for each spot
(this is useful for identifying spots that are clicked on)
"""
self.clear()
self.range = [[0,0],[0,0]]
self.addPoints(spots, x, y, data)
def addPoints(self, spots=None, x=None, y=None, data=None):
xmn = ymn = xmx = ymx = None
if spots is not None:
n = len(spots)
else:
n = len(x)
for i in range(n):
if spots is not None:
s = spots[i]
pos = Point(s['pos'])
else:
s = {}
pos = Point(x[i], y[i])
if data is not None:
s['data'] = data[i]
size = s.get('size', self.size)
if self.pxMode:
psize = 0
else:
psize = size
if xmn is None:
xmn = pos[0]-psize
xmx = pos[0]+psize
ymn = pos[1]-psize
ymx = pos[1]+psize
else:
xmn = min(xmn, pos[0]-psize)
xmx = max(xmx, pos[0]+psize)
ymn = min(ymn, pos[1]-psize)
ymx = max(ymx, pos[1]+psize)
#print pos, xmn, xmx, ymn, ymx
brush = s.get('brush', self.brush)
pen = s.get('pen', self.pen)
pen.setCosmetic(True)
symbol = s.get('symbol', self.symbol)
data2 = s.get('data', None)
item = self.mkSpot(pos, size, self.pxMode, brush, pen, data2, symbol=symbol, index=len(self.spots))
self.spots.append(item)
#if self.optimize:
#item.hide()
#frag = QtGui.QPainter.PixmapFragment.create(pos, QtCore.QRectF(0, 0, size, size))
#self.optimizeFragments.append(frag)
self.range = [[xmn, xmx], [ymn, ymx]]
#def setPointSize(self, size):
#for s in self.spots:
#s.size = size
##self.setPoints([{'size':s.size, 'pos':s.pos(), 'data':s.data} for s in self.spots])
#self.setPoints()
#def paint(self, p, *args):
#if not self.optimize:
#return
##p.setClipRegion(self.boundingRect())
#p.drawPixmapFragments(self.optimizeFragments, self.optimizePixmap)
def paint(self, *args):
pass
def spotPixmap(self):
## If all spots are identical, return the pixmap to use for all spots
## Otherwise return None
if not self.identical:
return None
if self._spotPixmap is None:
#print 'spotPixmap'
spot = SpotItem(size=self.size, pxMode=True, brush=self.brush, pen=self.pen, symbol=self.symbol)
#self._spotPixmap = PixmapSpotItem.makeSpotImage(self.size, self.pen, self.brush, self.symbol)
self._spotPixmap = spot.pixmap
return self._spotPixmap
def mkSpot(self, pos, size, pxMode, brush, pen, data, symbol=None, index=None):
## Make and return a SpotItem (or PixmapSpotItem if in pxMode)
brush = fn.mkBrush(brush)
pen = fn.mkPen(pen)
if pxMode:
img = self.spotPixmap() ## returns None if not using identical mode
#item = PixmapSpotItem(size, brush, pen, data, image=img, symbol=symbol, index=index)
item = SpotItem(size, pxMode, brush, pen, data, symbol=symbol, image=img, index=index)
else:
item = SpotItem(size, pxMode, brush, pen, data, symbol=symbol, index=index)
item.setParentItem(self)
item.setPos(pos)
#item.sigClicked.connect(self.pointClicked)
return item
def boundingRect(self):
((xmn, xmx), (ymn, ymx)) = self.range
if xmn is None or xmx is None or ymn is None or ymx is None:
return QtCore.QRectF()
return QtCore.QRectF(xmn, ymn, xmx-xmn, ymx-ymn)
#return QtCore.QRectF(xmn-1, ymn-1, xmx-xmn+2, ymx-ymn+2)
#def pointClicked(self, point):
#self.sigPointClicked.emit(self, point)
def points(self):
return self.spots[:]
def pointsAt(self, pos):
x = pos.x()
y = pos.y()
pw = self.pixelWidth()
ph = self.pixelHeight()
pts = []
for s in self.spots:
sp = s.pos()
ss = s.size
sx = sp.x()
sy = sp.y()
s2x = s2y = ss * 0.5
if self.pxMode:
s2x *= pw
s2y *= ph
if x > sx-s2x and x < sx+s2x and y > sy-s2y and y < sy+s2y:
pts.append(s)
#print "HIT:", x, y, sx, sy, s2x, s2y
#else:
#print "No hit:", (x, y), (sx, sy)
#print " ", (sx-s2x, sy-s2y), (sx+s2x, sy+s2y)
pts.sort(lambda a,b: cmp(b.zValue(), a.zValue()))
return pts
#def mousePressEvent(self, ev):
#QtGui.QGraphicsItem.mousePressEvent(self, ev)
#if ev.button() == QtCore.Qt.LeftButton:
#pts = self.pointsAt(ev.pos())
#if len(pts) > 0:
#self.mouseMoved = False
#self.ptsClicked = pts
#ev.accept()
#else:
##print "no spots"
#ev.ignore()
#else:
#ev.ignore()
#def mouseMoveEvent(self, ev):
#QtGui.QGraphicsItem.mouseMoveEvent(self, ev)
#self.mouseMoved = True
#pass
#def mouseReleaseEvent(self, ev):
#QtGui.QGraphicsItem.mouseReleaseEvent(self, ev)
#if not self.mouseMoved:
#self.sigClicked.emit(self, self.ptsClicked)
def mouseClickEvent(self, ev):
if ev.button() == QtCore.Qt.LeftButton:
pts = self.pointsAt(ev.pos())
if len(pts) > 0:
self.ptsClicked = pts
self.sigClicked.emit(self, self.ptsClicked)
ev.accept()
else:
#print "no spots"
ev.ignore()
else:
ev.ignore()
class SpotItem(GraphicsObject):
#sigClicked = QtCore.Signal(object)
def __init__(self, size, pxMode, brush, pen, data=None, symbol=None, image=None, index=None):
GraphicsObject.__init__(self)
self.pxMode = pxMode
if symbol is None:
symbol = 'o' ## circle by default
elif isinstance(symbol, int): ## allow symbols specified by integer for easy iteration
symbol = ['o', 's', 't', 'd', '+'][symbol]
####print 'SpotItem symbol: ', symbol
self.data = data
self.pen = pen
self.brush = brush
self.size = size
self.index = index
self.symbol = symbol
#s2 = size/2.
self.path = QtGui.QPainterPath()
if symbol == 'o':
self.path.addEllipse(QtCore.QRectF(-0.5, -0.5, 1, 1))
elif symbol == 's':
self.path.addRect(QtCore.QRectF(-0.5, -0.5, 1, 1))
elif symbol is 't' or symbol is '^':
self.path.moveTo(-0.5, -0.5)
self.path.lineTo(0, 0.5)
self.path.lineTo(0.5, -0.5)
self.path.closeSubpath()
#self.path.connectPath(self.path)
elif symbol == 'd':
self.path.moveTo(0., -0.5)
self.path.lineTo(-0.4, 0.)
self.path.lineTo(0, 0.5)
self.path.lineTo(0.4, 0)
self.path.closeSubpath()
#self.path.connectPath(self.path)
elif symbol == '+':
self.path.moveTo(-0.5, -0.01)
self.path.lineTo(-0.5, 0.01)
self.path.lineTo(-0.01, 0.01)
self.path.lineTo(-0.01, 0.5)
self.path.lineTo(0.01, 0.5)
self.path.lineTo(0.01, 0.01)
self.path.lineTo(0.5, 0.01)
self.path.lineTo(0.5, -0.01)
self.path.lineTo(0.01, -0.01)
self.path.lineTo(0.01, -0.5)
self.path.lineTo(-0.01, -0.5)
self.path.lineTo(-0.01, -0.01)
self.path.closeSubpath()
#self.path.connectPath(self.path)
#elif symbol == 'x':
else:
raise Exception("Unknown spot symbol '%s'" % symbol)
#self.path.addEllipse(QtCore.QRectF(-0.5, -0.5, 1, 1))
if pxMode:
## pre-render an image of the spot and display this rather than redrawing every time.
if image is None:
self.pixmap = self.makeSpotImage(size, pen, brush, symbol)
else:
self.pixmap = image ## image is already provided (probably shared with other spots)
self.setFlags(self.flags() | self.ItemIgnoresTransformations | self.ItemHasNoContents)
self.pi = QtGui.QGraphicsPixmapItem(self.pixmap, self)
self.pi.setPos(-0.5*size, -0.5*size)
else:
self.scale(size, size)
def makeSpotImage(self, size, pen, brush, symbol=None):
self.spotImage = QtGui.QImage(size+2, size+2, QtGui.QImage.Format_ARGB32_Premultiplied)
self.spotImage.fill(0)
p = QtGui.QPainter(self.spotImage)
p.setRenderHint(p.Antialiasing)
p.translate(size*0.5+1, size*0.5+1)
p.scale(size, size)
self.paint(p, None, None)
p.end()
return QtGui.QPixmap(self.spotImage)
def setBrush(self, brush):
self.brush = fn.mkBrush(brush)
self.update()
def setPen(self, pen):
self.pen = fn.mkPen(pen)
self.update()
def boundingRect(self):
return self.path.boundingRect()
def shape(self):
return self.path
def paint(self, p, *opts):
p.setPen(self.pen)
p.setBrush(self.brush)
p.drawPath(self.path)