2012-03-02 02:55:32 +00:00
|
|
|
from pyqtgraph.Qt import QtGui, QtCore
|
|
|
|
from pyqtgraph.Point import Point
|
|
|
|
import pyqtgraph.functions as fn
|
2012-05-11 22:05:41 +00:00
|
|
|
from .GraphicsItem import GraphicsItem
|
|
|
|
from .GraphicsObject import GraphicsObject
|
2012-03-18 18:57:36 +00:00
|
|
|
import numpy as np
|
|
|
|
import scipy.stats
|
2012-05-11 03:37:07 +00:00
|
|
|
import weakref
|
|
|
|
import pyqtgraph.debug as debug
|
|
|
|
from collections import OrderedDict
|
|
|
|
#import pyqtgraph as pg
|
2012-03-02 02:55:32 +00:00
|
|
|
|
|
|
|
__all__ = ['ScatterPlotItem', 'SpotItem']
|
2012-04-30 22:20:27 +00:00
|
|
|
|
|
|
|
|
|
|
|
## Build all symbol paths
|
2012-05-11 03:37:07 +00:00
|
|
|
Symbols = OrderedDict([(name, QtGui.QPainterPath()) for name in ['o', 's', 't', 'd', '+']])
|
2012-04-30 22:20:27 +00:00
|
|
|
Symbols['o'].addEllipse(QtCore.QRectF(-0.5, -0.5, 1, 1))
|
|
|
|
Symbols['s'].addRect(QtCore.QRectF(-0.5, -0.5, 1, 1))
|
|
|
|
coords = {
|
|
|
|
't': [(-0.5, -0.5), (0, 0.5), (0.5, -0.5)],
|
|
|
|
'd': [(0., -0.5), (-0.4, 0.), (0, 0.5), (0.4, 0)],
|
|
|
|
'+': [
|
|
|
|
(-0.5, -0.05), (-0.5, 0.05), (-0.05, 0.05), (-0.05, 0.5),
|
|
|
|
(0.05, 0.5), (0.05, 0.05), (0.5, 0.05), (0.5, -0.05),
|
|
|
|
(0.05, -0.05), (0.05, -0.5), (-0.05, -0.5), (-0.05, -0.05)
|
|
|
|
],
|
|
|
|
}
|
2012-05-11 22:05:41 +00:00
|
|
|
for k, c in coords.items():
|
2012-04-30 22:20:27 +00:00
|
|
|
Symbols[k].moveTo(*c[0])
|
|
|
|
for x,y in c[1:]:
|
|
|
|
Symbols[k].lineTo(x, y)
|
|
|
|
Symbols[k].closeSubpath()
|
|
|
|
|
|
|
|
|
2012-05-11 03:37:07 +00:00
|
|
|
def makeSymbolPixmap(size, pen, brush, symbol):
|
|
|
|
## Render a spot with the given parameters to a pixmap
|
2012-05-15 17:31:50 +00:00
|
|
|
penPxWidth = max(np.ceil(pen.width()), 1)
|
2012-06-18 21:47:56 +00:00
|
|
|
image = QtGui.QImage(int(size+penPxWidth), int(size+penPxWidth), QtGui.QImage.Format_ARGB32_Premultiplied)
|
2012-05-11 03:37:07 +00:00
|
|
|
image.fill(0)
|
|
|
|
p = QtGui.QPainter(image)
|
|
|
|
p.setRenderHint(p.Antialiasing)
|
2012-05-15 02:05:53 +00:00
|
|
|
p.translate(image.width()*0.5, image.height()*0.5)
|
2012-05-11 03:37:07 +00:00
|
|
|
p.scale(size, size)
|
|
|
|
p.setPen(pen)
|
|
|
|
p.setBrush(brush)
|
|
|
|
p.drawPath(Symbols[symbol])
|
|
|
|
p.end()
|
|
|
|
return QtGui.QPixmap(image)
|
|
|
|
|
|
|
|
|
2012-04-30 22:20:27 +00:00
|
|
|
|
2012-03-02 02:55:32 +00:00
|
|
|
class ScatterPlotItem(GraphicsObject):
|
2012-04-21 19:55:27 +00:00
|
|
|
"""
|
|
|
|
Displays a set of x/y points. Instances of this class are created
|
|
|
|
automatically as part of PlotDataItem; these rarely need to be instantiated
|
|
|
|
directly.
|
2012-03-02 02:55:32 +00:00
|
|
|
|
2012-04-21 19:55:27 +00:00
|
|
|
The size, shape, pen, and fill brush may be set for each point individually
|
|
|
|
or for all points.
|
|
|
|
|
|
|
|
|
|
|
|
======================== ===============================================
|
|
|
|
**Signals:**
|
|
|
|
sigPlotChanged(self) Emitted when the data being plotted has changed
|
|
|
|
sigClicked(self, points) Emitted when the curve is clicked. Sends a list
|
|
|
|
of all the points under the mouse pointer.
|
|
|
|
======================== ===============================================
|
|
|
|
|
|
|
|
"""
|
2012-03-02 02:55:32 +00:00
|
|
|
#sigPointClicked = QtCore.Signal(object, object)
|
|
|
|
sigClicked = QtCore.Signal(object, object) ## self, points
|
|
|
|
sigPlotChanged = QtCore.Signal(object)
|
|
|
|
|
2012-03-18 18:57:36 +00:00
|
|
|
def __init__(self, *args, **kargs):
|
|
|
|
"""
|
|
|
|
Accepts the same arguments as setData()
|
|
|
|
"""
|
2012-05-11 03:37:07 +00:00
|
|
|
prof = debug.Profiler('ScatterPlotItem.__init__', disabled=True)
|
2012-03-18 18:57:36 +00:00
|
|
|
GraphicsObject.__init__(self)
|
2012-05-15 17:38:32 +00:00
|
|
|
self.setFlag(self.ItemHasNoContents, True)
|
2012-05-11 03:37:07 +00:00
|
|
|
self.data = np.empty(0, dtype=[('x', float), ('y', float), ('size', float), ('symbol', 'S1'), ('pen', object), ('brush', object), ('item', object), ('data', object)])
|
2012-05-15 02:05:53 +00:00
|
|
|
self.bounds = [None, None] ## caches data bounds
|
|
|
|
self._maxSpotWidth = 0 ## maximum size of the scale-variant portion of all spots
|
|
|
|
self._maxSpotPxWidth = 0 ## maximum size of the scale-invariant portion of all spots
|
2012-03-18 18:57:36 +00:00
|
|
|
self._spotPixmap = None
|
2012-05-15 02:05:53 +00:00
|
|
|
self.opts = {'pxMode': True}
|
2012-03-18 18:57:36 +00:00
|
|
|
|
2012-05-11 03:37:07 +00:00
|
|
|
self.setPen(200,200,200, update=False)
|
|
|
|
self.setBrush(100,100,150, update=False)
|
|
|
|
self.setSymbol('o', update=False)
|
|
|
|
self.setSize(7, update=False)
|
|
|
|
prof.mark('1')
|
2012-03-18 18:57:36 +00:00
|
|
|
self.setData(*args, **kargs)
|
2012-05-11 03:37:07 +00:00
|
|
|
prof.mark('setData')
|
|
|
|
prof.finish()
|
2012-03-18 18:57:36 +00:00
|
|
|
|
|
|
|
def setData(self, *args, **kargs):
|
2012-03-02 02:55:32 +00:00
|
|
|
"""
|
2012-04-21 19:55:27 +00:00
|
|
|
**Ordered Arguments:**
|
|
|
|
|
|
|
|
* If there is only one unnamed argument, it will be interpreted like the 'spots' argument.
|
|
|
|
* If there are two unnamed arguments, they will be interpreted as sequences of x and y values.
|
|
|
|
|
2012-04-25 17:12:40 +00:00
|
|
|
====================== ===============================================================================================
|
2012-04-21 19:55:27 +00:00
|
|
|
**Keyword Arguments:**
|
|
|
|
*spots* Optional list of dicts. Each dict specifies parameters for a single spot:
|
|
|
|
{'pos': (x,y), 'size', 'pen', 'brush', 'symbol'}. This is just an alternate method
|
|
|
|
of passing in data for the corresponding arguments.
|
|
|
|
*x*,*y* 1D arrays of x,y values.
|
|
|
|
*pos* 2D structure of x,y pairs (such as Nx2 array or list of tuples)
|
|
|
|
*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.
|
|
|
|
Default is True
|
|
|
|
*symbol* can be one (or a list) of:
|
|
|
|
|
|
|
|
* 'o' circle (default)
|
|
|
|
* 's' square
|
|
|
|
* 't' triangle
|
|
|
|
* 'd' diamond
|
|
|
|
* '+' plus
|
|
|
|
*pen* The pen (or list of pens) to use for drawing spot outlines.
|
|
|
|
*brush* The brush (or list of brushes) to use for filling spots.
|
|
|
|
*size* The size (or list of sizes) of spots. If *pxMode* is True, this value is in pixels. Otherwise,
|
|
|
|
it is in the item's local coordinate system.
|
|
|
|
*data* a list of python objects used to uniquely identify each spot.
|
2012-05-11 03:37:07 +00:00
|
|
|
*identical* *Deprecated*. This functionality is handled automatically now.
|
2012-04-25 17:12:40 +00:00
|
|
|
====================== ===============================================================================================
|
2012-03-02 02:55:32 +00:00
|
|
|
"""
|
2012-05-11 03:37:07 +00:00
|
|
|
self.clear() ## clear out all old data
|
|
|
|
self.addPoints(*args, **kargs)
|
2012-03-18 18:57:36 +00:00
|
|
|
|
2012-05-11 03:37:07 +00:00
|
|
|
def addPoints(self, *args, **kargs):
|
|
|
|
"""
|
|
|
|
Add new points to the scatter plot.
|
|
|
|
Arguments are the same as setData()
|
|
|
|
"""
|
2012-03-02 02:55:32 +00:00
|
|
|
|
2012-03-18 18:57:36 +00:00
|
|
|
## deal with non-keyword arguments
|
|
|
|
if len(args) == 1:
|
|
|
|
kargs['spots'] = args[0]
|
|
|
|
elif len(args) == 2:
|
|
|
|
kargs['x'] = args[0]
|
|
|
|
kargs['y'] = args[1]
|
|
|
|
elif len(args) > 2:
|
|
|
|
raise Exception('Only accepts up to two non-keyword arguments.')
|
2012-03-02 02:55:32 +00:00
|
|
|
|
2012-03-18 18:57:36 +00:00
|
|
|
## convert 'pos' argument to 'x' and 'y'
|
|
|
|
if 'pos' in kargs:
|
|
|
|
pos = kargs['pos']
|
|
|
|
if isinstance(pos, np.ndarray):
|
|
|
|
kargs['x'] = pos[:,0]
|
|
|
|
kargs['y'] = pos[:,1]
|
|
|
|
else:
|
|
|
|
x = []
|
|
|
|
y = []
|
|
|
|
for p in pos:
|
|
|
|
if isinstance(p, QtCore.QPointF):
|
|
|
|
x.append(p.x())
|
|
|
|
y.append(p.y())
|
|
|
|
else:
|
|
|
|
x.append(p[0])
|
|
|
|
y.append(p[1])
|
|
|
|
kargs['x'] = x
|
|
|
|
kargs['y'] = y
|
2012-03-02 02:55:32 +00:00
|
|
|
|
2012-03-18 18:57:36 +00:00
|
|
|
## determine how many spots we have
|
|
|
|
if 'spots' in kargs:
|
|
|
|
numPts = len(kargs['spots'])
|
|
|
|
elif 'y' in kargs and kargs['y'] is not None:
|
|
|
|
numPts = len(kargs['y'])
|
2012-03-02 02:55:32 +00:00
|
|
|
else:
|
2012-03-18 18:57:36 +00:00
|
|
|
kargs['x'] = []
|
|
|
|
kargs['y'] = []
|
|
|
|
numPts = 0
|
2012-03-02 02:55:32 +00:00
|
|
|
|
2012-05-11 03:37:07 +00:00
|
|
|
## Extend record array
|
|
|
|
oldData = self.data
|
|
|
|
self.data = np.empty(len(oldData)+numPts, dtype=self.data.dtype)
|
|
|
|
## note that np.empty initializes object fields to None and string fields to ''
|
|
|
|
|
|
|
|
self.data[:len(oldData)] = oldData
|
|
|
|
for i in range(len(oldData)):
|
|
|
|
oldData[i]['item']._data = self.data[i] ## Make sure items have proper reference to new array
|
|
|
|
|
|
|
|
newData = self.data[len(oldData):]
|
|
|
|
newData['size'] = -1 ## indicates to use default size
|
2012-03-02 02:55:32 +00:00
|
|
|
|
2012-03-18 18:57:36 +00:00
|
|
|
if 'spots' in kargs:
|
|
|
|
spots = kargs['spots']
|
2012-05-11 22:05:41 +00:00
|
|
|
for i in range(len(spots)):
|
2012-03-18 18:57:36 +00:00
|
|
|
spot = spots[i]
|
|
|
|
for k in spot:
|
2012-05-11 03:37:07 +00:00
|
|
|
#if k == 'pen':
|
|
|
|
#newData[k] = fn.mkPen(spot[k])
|
|
|
|
#elif k == 'brush':
|
|
|
|
#newData[k] = fn.mkBrush(spot[k])
|
|
|
|
if k == 'pos':
|
2012-03-18 18:57:36 +00:00
|
|
|
pos = spot[k]
|
|
|
|
if isinstance(pos, QtCore.QPointF):
|
|
|
|
x,y = pos.x(), pos.y()
|
|
|
|
else:
|
|
|
|
x,y = pos[0], pos[1]
|
2012-05-11 03:37:07 +00:00
|
|
|
newData[i]['x'] = x
|
|
|
|
newData[i]['y'] = y
|
|
|
|
elif k in ['x', 'y', 'size', 'symbol', 'pen', 'brush', 'data']:
|
|
|
|
newData[i][k] = spot[k]
|
|
|
|
#elif k == 'data':
|
|
|
|
#self.pointData[i] = spot[k]
|
2012-03-18 18:57:36 +00:00
|
|
|
else:
|
|
|
|
raise Exception("Unknown spot parameter: %s" % k)
|
|
|
|
elif 'y' in kargs:
|
2012-05-11 03:37:07 +00:00
|
|
|
newData['x'] = kargs['x']
|
|
|
|
newData['y'] = kargs['y']
|
2012-03-18 18:57:36 +00:00
|
|
|
|
2012-05-11 03:37:07 +00:00
|
|
|
if 'pxMode' in kargs:
|
|
|
|
self.setPxMode(kargs['pxMode'], update=False)
|
|
|
|
|
2012-03-18 18:57:36 +00:00
|
|
|
## Set any extra parameters provided in keyword arguments
|
2012-05-11 03:37:07 +00:00
|
|
|
for k in ['pen', 'brush', 'symbol', 'size']:
|
2012-03-18 18:57:36 +00:00
|
|
|
if k in kargs:
|
|
|
|
setMethod = getattr(self, 'set' + k[0].upper() + k[1:])
|
2012-05-11 03:37:07 +00:00
|
|
|
setMethod(kargs[k], update=False, dataSet=newData)
|
2012-03-18 18:57:36 +00:00
|
|
|
|
2012-05-11 03:37:07 +00:00
|
|
|
if 'data' in kargs:
|
|
|
|
self.setPointData(kargs['data'], dataSet=newData)
|
2012-03-18 18:57:36 +00:00
|
|
|
|
2012-05-11 03:37:07 +00:00
|
|
|
#self.updateSpots()
|
2012-06-22 01:52:34 +00:00
|
|
|
self.prepareGeometryChange()
|
2012-05-15 02:05:53 +00:00
|
|
|
self.bounds = [None, None]
|
2012-05-11 03:37:07 +00:00
|
|
|
self.generateSpotItems()
|
|
|
|
self.sigPlotChanged.emit(self)
|
2012-03-18 18:57:36 +00:00
|
|
|
|
|
|
|
|
|
|
|
def setPoints(self, *args, **kargs):
|
2012-04-21 19:55:27 +00:00
|
|
|
##Deprecated; use setData
|
2012-03-18 18:57:36 +00:00
|
|
|
return self.setData(*args, **kargs)
|
|
|
|
|
2012-03-02 02:55:32 +00:00
|
|
|
def implements(self, interface=None):
|
|
|
|
ints = ['plotData']
|
|
|
|
if interface is None:
|
|
|
|
return ints
|
|
|
|
return interface in ints
|
|
|
|
|
2012-03-18 18:57:36 +00:00
|
|
|
def setPen(self, *args, **kargs):
|
2012-05-11 03:37:07 +00:00
|
|
|
"""Set the pen(s) used to draw the outline around each spot.
|
|
|
|
If a list or array is provided, then the pen for each spot will be set separately.
|
|
|
|
Otherwise, the arguments are passed to pg.mkPen and used as the default pen for
|
|
|
|
all spots which do not have a pen explicitly set."""
|
|
|
|
update = kargs.pop('update', True)
|
|
|
|
dataSet = kargs.pop('dataSet', self.data)
|
|
|
|
|
2012-03-18 18:57:36 +00:00
|
|
|
if len(args) == 1 and (isinstance(args[0], np.ndarray) or isinstance(args[0], list)):
|
|
|
|
pens = args[0]
|
2012-05-11 03:37:07 +00:00
|
|
|
if len(pens) != len(dataSet):
|
|
|
|
raise Exception("Number of pens does not match number of points (%d != %d)" % (len(pens), len(dataSet)))
|
|
|
|
dataSet['pen'] = pens
|
2012-03-18 18:57:36 +00:00
|
|
|
else:
|
|
|
|
self.opts['pen'] = fn.mkPen(*args, **kargs)
|
2012-05-11 03:37:07 +00:00
|
|
|
self._spotPixmap = None
|
|
|
|
|
|
|
|
if update:
|
|
|
|
self.updateSpots(dataSet)
|
2012-03-18 18:57:36 +00:00
|
|
|
|
|
|
|
def setBrush(self, *args, **kargs):
|
2012-05-11 03:37:07 +00:00
|
|
|
"""Set the brush(es) used to fill the interior of each spot.
|
|
|
|
If a list or array is provided, then the brush for each spot will be set separately.
|
|
|
|
Otherwise, the arguments are passed to pg.mkBrush and used as the default brush for
|
|
|
|
all spots which do not have a brush explicitly set."""
|
|
|
|
update = kargs.pop('update', True)
|
|
|
|
dataSet = kargs.pop('dataSet', self.data)
|
|
|
|
|
2012-03-18 18:57:36 +00:00
|
|
|
if len(args) == 1 and (isinstance(args[0], np.ndarray) or isinstance(args[0], list)):
|
|
|
|
brushes = args[0]
|
2012-05-11 03:37:07 +00:00
|
|
|
if len(brushes) != len(dataSet):
|
|
|
|
raise Exception("Number of brushes does not match number of points (%d != %d)" % (len(brushes), len(dataSet)))
|
|
|
|
#for i in xrange(len(brushes)):
|
|
|
|
#self.data[i]['brush'] = fn.mkBrush(brushes[i], **kargs)
|
|
|
|
dataSet['brush'] = brushes
|
2012-03-18 18:57:36 +00:00
|
|
|
else:
|
|
|
|
self.opts['brush'] = fn.mkBrush(*args, **kargs)
|
2012-05-11 03:37:07 +00:00
|
|
|
self._spotPixmap = None
|
|
|
|
|
|
|
|
if update:
|
|
|
|
self.updateSpots(dataSet)
|
2012-03-18 18:57:36 +00:00
|
|
|
|
2012-05-11 03:37:07 +00:00
|
|
|
def setSymbol(self, symbol, update=True, dataSet=None):
|
|
|
|
"""Set the symbol(s) used to draw each spot.
|
|
|
|
If a list or array is provided, then the symbol for each spot will be set separately.
|
|
|
|
Otherwise, the argument will be used as the default symbol for
|
|
|
|
all spots which do not have a symbol explicitly set."""
|
|
|
|
if dataSet is None:
|
|
|
|
dataSet = self.data
|
|
|
|
|
2012-03-18 18:57:36 +00:00
|
|
|
if isinstance(symbol, np.ndarray) or isinstance(symbol, list):
|
|
|
|
symbols = symbol
|
2012-05-11 03:37:07 +00:00
|
|
|
if len(symbols) != len(dataSet):
|
|
|
|
raise Exception("Number of symbols does not match number of points (%d != %d)" % (len(symbols), len(dataSet)))
|
|
|
|
dataSet['symbol'] = symbols
|
2012-03-18 18:57:36 +00:00
|
|
|
else:
|
|
|
|
self.opts['symbol'] = symbol
|
2012-05-11 03:37:07 +00:00
|
|
|
self._spotPixmap = None
|
2012-03-18 18:57:36 +00:00
|
|
|
|
2012-05-11 03:37:07 +00:00
|
|
|
if update:
|
|
|
|
self.updateSpots(dataSet)
|
|
|
|
|
|
|
|
def setSize(self, size, update=True, dataSet=None):
|
|
|
|
"""Set the size(s) used to draw each spot.
|
|
|
|
If a list or array is provided, then the size for each spot will be set separately.
|
|
|
|
Otherwise, the argument will be used as the default size for
|
|
|
|
all spots which do not have a size explicitly set."""
|
|
|
|
if dataSet is None:
|
|
|
|
dataSet = self.data
|
|
|
|
|
2012-03-18 18:57:36 +00:00
|
|
|
if isinstance(size, np.ndarray) or isinstance(size, list):
|
|
|
|
sizes = size
|
2012-05-11 03:37:07 +00:00
|
|
|
if len(sizes) != len(dataSet):
|
|
|
|
raise Exception("Number of sizes does not match number of points (%d != %d)" % (len(sizes), len(dataSet)))
|
|
|
|
dataSet['size'] = sizes
|
2012-03-18 18:57:36 +00:00
|
|
|
else:
|
|
|
|
self.opts['size'] = size
|
2012-05-11 03:37:07 +00:00
|
|
|
self._spotPixmap = None
|
|
|
|
|
|
|
|
if update:
|
|
|
|
self.updateSpots(dataSet)
|
2012-03-18 18:57:36 +00:00
|
|
|
|
2012-05-11 03:37:07 +00:00
|
|
|
def setPointData(self, data, dataSet=None):
|
|
|
|
if dataSet is None:
|
|
|
|
dataSet = self.data
|
|
|
|
|
2012-04-21 19:55:27 +00:00
|
|
|
if isinstance(data, np.ndarray) or isinstance(data, list):
|
2012-05-11 03:37:07 +00:00
|
|
|
if len(data) != len(dataSet):
|
|
|
|
raise Exception("Length of meta data does not match number of points (%d != %d)" % (len(data), len(dataSet)))
|
2012-06-18 17:50:44 +00:00
|
|
|
|
|
|
|
## Bug: If data is a numpy record array, then items from that array must be copied to dataSet one at a time.
|
|
|
|
## (otherwise they are converted to tuples and thus lose their field names.
|
|
|
|
if isinstance(data, np.ndarray) and len(data.dtype.fields) > 1:
|
|
|
|
for i, rec in enumerate(data):
|
|
|
|
dataSet['data'][i] = rec
|
|
|
|
else:
|
|
|
|
dataSet['data'] = data
|
2012-04-21 19:55:27 +00:00
|
|
|
|
2012-05-11 03:37:07 +00:00
|
|
|
def setPxMode(self, mode, update=True):
|
|
|
|
if self.opts['pxMode'] == mode:
|
|
|
|
return
|
|
|
|
|
2012-03-18 18:57:36 +00:00
|
|
|
self.opts['pxMode'] = mode
|
2012-05-11 03:37:07 +00:00
|
|
|
self.clearItems()
|
|
|
|
if update:
|
|
|
|
self.generateSpotItems()
|
2012-03-18 18:57:36 +00:00
|
|
|
|
2012-05-11 03:37:07 +00:00
|
|
|
def updateSpots(self, dataSet=None):
|
|
|
|
if dataSet is None:
|
|
|
|
dataSet = self.data
|
2012-05-15 02:05:53 +00:00
|
|
|
self._maxSpotWidth = 0
|
|
|
|
self._maxSpotPxWidth = 0
|
2012-05-11 03:37:07 +00:00
|
|
|
for spot in dataSet['item']:
|
|
|
|
spot.updateItem()
|
2012-05-15 02:05:53 +00:00
|
|
|
self.measureSpotSizes(dataSet)
|
|
|
|
|
|
|
|
def measureSpotSizes(self, dataSet):
|
|
|
|
for spot in dataSet['item']:
|
|
|
|
## keep track of the maximum spot size and pixel size
|
|
|
|
width = 0
|
|
|
|
pxWidth = 0
|
|
|
|
pen = spot.pen()
|
2012-05-15 17:31:50 +00:00
|
|
|
if self.opts['pxMode']:
|
|
|
|
pxWidth = spot.size() + pen.width()
|
2012-05-15 02:05:53 +00:00
|
|
|
else:
|
2012-05-15 17:31:50 +00:00
|
|
|
width = spot.size()
|
|
|
|
if pen.isCosmetic():
|
|
|
|
pxWidth += pen.width()
|
|
|
|
else:
|
|
|
|
width += pen.width()
|
2012-05-15 02:05:53 +00:00
|
|
|
self._maxSpotWidth = max(self._maxSpotWidth, width)
|
|
|
|
self._maxSpotPxWidth = max(self._maxSpotPxWidth, pxWidth)
|
2012-05-15 17:31:50 +00:00
|
|
|
self.bounds = [None, None]
|
2012-05-15 02:05:53 +00:00
|
|
|
|
|
|
|
|
2012-03-02 02:55:32 +00:00
|
|
|
def clear(self):
|
2012-05-11 03:37:07 +00:00
|
|
|
"""Remove all spots from the scatter plot"""
|
|
|
|
self.clearItems()
|
|
|
|
self.data = np.empty(0, dtype=self.data.dtype)
|
|
|
|
self.bounds = [None, None]
|
|
|
|
|
|
|
|
def clearItems(self):
|
|
|
|
for i in self.data['item']:
|
|
|
|
if i is None:
|
|
|
|
continue
|
2012-03-02 02:55:32 +00:00
|
|
|
i.setParentItem(None)
|
|
|
|
s = i.scene()
|
|
|
|
if s is not None:
|
|
|
|
s.removeItem(i)
|
2012-05-11 03:37:07 +00:00
|
|
|
self.data['item'] = None
|
2012-03-02 02:55:32 +00:00
|
|
|
|
2012-05-08 21:56:55 +00:00
|
|
|
def dataBounds(self, ax, frac=1.0, orthoRange=None):
|
2012-03-18 18:57:36 +00:00
|
|
|
if frac >= 1.0 and self.bounds[ax] is not None:
|
|
|
|
return self.bounds[ax]
|
2012-03-02 02:55:32 +00:00
|
|
|
|
2012-06-22 01:52:34 +00:00
|
|
|
#self.prepareGeometryChange()
|
2012-03-18 18:57:36 +00:00
|
|
|
if self.data is None or len(self.data) == 0:
|
|
|
|
return (None, None)
|
|
|
|
|
|
|
|
if ax == 0:
|
|
|
|
d = self.data['x']
|
2012-05-08 21:56:55 +00:00
|
|
|
d2 = self.data['y']
|
2012-03-18 18:57:36 +00:00
|
|
|
elif ax == 1:
|
|
|
|
d = self.data['y']
|
2012-05-08 21:56:55 +00:00
|
|
|
d2 = self.data['x']
|
|
|
|
|
|
|
|
if orthoRange is not None:
|
|
|
|
mask = (d2 >= orthoRange[0]) * (d2 <= orthoRange[1])
|
|
|
|
d = d[mask]
|
|
|
|
d2 = d2[mask]
|
2012-03-18 18:57:36 +00:00
|
|
|
|
|
|
|
if frac >= 1.0:
|
2012-05-15 02:05:53 +00:00
|
|
|
## increase size of bounds based on spot size and pen width
|
|
|
|
px = self.pixelLength(Point(1, 0) if ax == 0 else Point(0, 1)) ## determine length of pixel along this axis
|
|
|
|
if px is None:
|
|
|
|
px = 0
|
2012-03-18 18:57:36 +00:00
|
|
|
minIndex = np.argmin(d)
|
|
|
|
maxIndex = np.argmax(d)
|
|
|
|
minVal = d[minIndex]
|
|
|
|
maxVal = d[maxIndex]
|
2012-05-15 02:05:53 +00:00
|
|
|
spotSize = 0.5 * (self._maxSpotWidth + px * self._maxSpotPxWidth)
|
|
|
|
self.bounds[ax] = (minVal-spotSize, maxVal+spotSize)
|
2012-03-18 18:57:36 +00:00
|
|
|
return self.bounds[ax]
|
|
|
|
elif frac <= 0.0:
|
|
|
|
raise Exception("Value for parameter 'frac' must be > 0. (got %s)" % str(frac))
|
|
|
|
else:
|
|
|
|
return (scipy.stats.scoreatpercentile(d, 50 - (frac * 50)), scipy.stats.scoreatpercentile(d, 50 + (frac * 50)))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
2012-05-11 03:37:07 +00:00
|
|
|
def generateSpotItems(self):
|
|
|
|
if self.opts['pxMode']:
|
|
|
|
for rec in self.data:
|
|
|
|
if rec['item'] is None:
|
|
|
|
rec['item'] = PixmapSpotItem(rec, self)
|
|
|
|
else:
|
|
|
|
for rec in self.data:
|
|
|
|
if rec['item'] is None:
|
|
|
|
rec['item'] = PathSpotItem(rec, self)
|
2012-05-15 02:05:53 +00:00
|
|
|
self.measureSpotSizes(self.data)
|
2012-03-18 18:57:36 +00:00
|
|
|
self.sigPlotChanged.emit(self)
|
2012-03-02 02:55:32 +00:00
|
|
|
|
2012-05-11 03:37:07 +00:00
|
|
|
def defaultSpotPixmap(self):
|
|
|
|
## Return the default spot pixmap
|
2012-03-02 02:55:32 +00:00
|
|
|
if self._spotPixmap is None:
|
2012-05-11 03:37:07 +00:00
|
|
|
self._spotPixmap = makeSymbolPixmap(size=self.opts['size'], brush=self.opts['brush'], pen=self.opts['pen'], symbol=self.opts['symbol'])
|
2012-03-02 02:55:32 +00:00
|
|
|
return self._spotPixmap
|
|
|
|
|
|
|
|
def boundingRect(self):
|
2012-03-18 18:57:36 +00:00
|
|
|
(xmn, xmx) = self.dataBounds(ax=0)
|
|
|
|
(ymn, ymx) = self.dataBounds(ax=1)
|
|
|
|
if xmn is None or xmx is None:
|
|
|
|
xmn = 0
|
|
|
|
xmx = 0
|
|
|
|
if ymn is None or ymx is None:
|
|
|
|
ymn = 0
|
|
|
|
ymx = 0
|
2012-03-02 02:55:32 +00:00
|
|
|
return QtCore.QRectF(xmn, ymn, xmx-xmn, ymx-ymn)
|
2012-05-15 02:05:53 +00:00
|
|
|
|
|
|
|
def viewRangeChanged(self):
|
2012-06-22 01:52:34 +00:00
|
|
|
self.prepareGeometryChange()
|
2012-05-15 02:05:53 +00:00
|
|
|
GraphicsObject.viewRangeChanged(self)
|
|
|
|
self.bounds = [None, None]
|
|
|
|
|
|
|
|
def paint(self, p, *args):
|
|
|
|
## NOTE: self.paint is disabled by this line in __init__:
|
|
|
|
## self.setFlag(self.ItemHasNoContents, True)
|
|
|
|
p.setPen(fn.mkPen('r'))
|
|
|
|
p.drawRect(self.boundingRect())
|
|
|
|
|
2012-03-18 18:57:36 +00:00
|
|
|
|
2012-03-02 02:55:32 +00:00
|
|
|
def points(self):
|
2012-05-11 03:37:07 +00:00
|
|
|
return self.data['item']
|
|
|
|
|
2012-03-02 02:55:32 +00:00
|
|
|
def pointsAt(self, pos):
|
|
|
|
x = pos.x()
|
|
|
|
y = pos.y()
|
|
|
|
pw = self.pixelWidth()
|
|
|
|
ph = self.pixelHeight()
|
|
|
|
pts = []
|
2012-05-11 03:37:07 +00:00
|
|
|
for s in self.points():
|
2012-03-02 02:55:32 +00:00
|
|
|
sp = s.pos()
|
2012-05-11 03:37:07 +00:00
|
|
|
ss = s.size()
|
2012-03-02 02:55:32 +00:00
|
|
|
sx = sp.x()
|
|
|
|
sy = sp.y()
|
|
|
|
s2x = s2y = ss * 0.5
|
2012-03-18 18:57:36 +00:00
|
|
|
if self.opts['pxMode']:
|
2012-03-02 02:55:32 +00:00
|
|
|
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 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()
|
|
|
|
|
|
|
|
|
2012-05-11 03:37:07 +00:00
|
|
|
class SpotItem(GraphicsItem):
|
|
|
|
"""
|
|
|
|
Class referring to individual spots in a scatter plot.
|
|
|
|
These can be retrieved by calling ScatterPlotItem.points() or
|
|
|
|
by connecting to the ScatterPlotItem's click signals.
|
|
|
|
"""
|
2012-03-02 02:55:32 +00:00
|
|
|
|
2012-05-11 03:37:07 +00:00
|
|
|
def __init__(self, data, plot):
|
|
|
|
GraphicsItem.__init__(self, register=False)
|
|
|
|
self._data = data
|
|
|
|
self._plot = plot
|
|
|
|
#self._viewBox = None
|
|
|
|
#self._viewWidget = None
|
|
|
|
self.setParentItem(plot)
|
|
|
|
self.setPos(QtCore.QPointF(data['x'], data['y']))
|
|
|
|
self.updateItem()
|
2012-03-02 02:55:32 +00:00
|
|
|
|
2012-05-11 03:37:07 +00:00
|
|
|
def data(self):
|
|
|
|
"""Return the user data associated with this spot."""
|
|
|
|
return self._data['data']
|
|
|
|
|
|
|
|
def size(self):
|
|
|
|
"""Return the size of this spot.
|
|
|
|
If the spot has no explicit size set, then return the ScatterPlotItem's default size instead."""
|
|
|
|
if self._data['size'] == -1:
|
|
|
|
return self._plot.opts['size']
|
|
|
|
else:
|
|
|
|
return self._data['size']
|
|
|
|
|
|
|
|
def setSize(self, size):
|
|
|
|
"""Set the size of this spot.
|
|
|
|
If the size is set to -1, then the ScatterPlotItem's default size
|
|
|
|
will be used instead."""
|
|
|
|
self._data['size'] = size
|
|
|
|
self.updateItem()
|
|
|
|
|
|
|
|
def symbol(self):
|
|
|
|
"""Return the symbol of this spot.
|
|
|
|
If the spot has no explicit symbol set, then return the ScatterPlotItem's default symbol instead.
|
|
|
|
"""
|
|
|
|
symbol = self._data['symbol']
|
|
|
|
if symbol == '':
|
|
|
|
symbol = self._plot.opts['symbol']
|
2012-03-18 18:57:36 +00:00
|
|
|
try:
|
2012-05-11 03:37:07 +00:00
|
|
|
n = int(symbol)
|
2012-05-11 22:05:41 +00:00
|
|
|
symbol = list(Symbols.keys())[n % len(Symbols)]
|
2012-05-11 03:37:07 +00:00
|
|
|
except:
|
2012-03-18 18:57:36 +00:00
|
|
|
pass
|
2012-05-11 03:37:07 +00:00
|
|
|
return symbol
|
|
|
|
|
|
|
|
def setSymbol(self, symbol):
|
|
|
|
"""Set the symbol for this spot.
|
|
|
|
If the symbol is set to '', then the ScatterPlotItem's default symbol will be used instead."""
|
|
|
|
self._data['symbol'] = symbol
|
|
|
|
self.updateItem()
|
|
|
|
|
|
|
|
def pen(self):
|
|
|
|
pen = self._data['pen']
|
|
|
|
if pen is None:
|
|
|
|
pen = self._plot.opts['pen']
|
|
|
|
return fn.mkPen(pen)
|
|
|
|
|
|
|
|
def setPen(self, *args, **kargs):
|
|
|
|
"""Set the outline pen for this spot"""
|
|
|
|
pen = fn.mkPen(*args, **kargs)
|
|
|
|
self._data['pen'] = pen
|
|
|
|
self.updateItem()
|
|
|
|
|
|
|
|
def resetPen(self):
|
|
|
|
"""Remove the pen set for this spot; the scatter plot's default pen will be used instead."""
|
|
|
|
self._data['pen'] = None ## Note this is NOT the same as calling setPen(None)
|
|
|
|
self.updateItem()
|
|
|
|
|
|
|
|
def brush(self):
|
|
|
|
brush = self._data['brush']
|
|
|
|
if brush is None:
|
|
|
|
brush = self._plot.opts['brush']
|
|
|
|
return fn.mkBrush(brush)
|
|
|
|
|
|
|
|
def setBrush(self, *args, **kargs):
|
|
|
|
"""Set the fill brush for this spot"""
|
|
|
|
brush = fn.mkBrush(*args, **kargs)
|
|
|
|
self._data['brush'] = brush
|
|
|
|
self.updateItem()
|
|
|
|
|
|
|
|
def resetBrush(self):
|
|
|
|
"""Remove the brush set for this spot; the scatter plot's default brush will be used instead."""
|
|
|
|
self._data['brush'] = None ## Note this is NOT the same as calling setBrush(None)
|
|
|
|
self.updateItem()
|
|
|
|
|
|
|
|
def setData(self, data):
|
|
|
|
"""Set the user-data associated with this spot"""
|
|
|
|
self._data['data'] = data
|
|
|
|
|
|
|
|
|
|
|
|
class PixmapSpotItem(SpotItem, QtGui.QGraphicsPixmapItem):
|
|
|
|
def __init__(self, data, plot):
|
|
|
|
QtGui.QGraphicsPixmapItem.__init__(self)
|
|
|
|
self.setFlags(self.flags() | self.ItemIgnoresTransformations)
|
|
|
|
SpotItem.__init__(self, data, plot)
|
|
|
|
|
|
|
|
def setPixmap(self, pixmap):
|
|
|
|
QtGui.QGraphicsPixmapItem.setPixmap(self, pixmap)
|
|
|
|
self.setOffset(-pixmap.width()/2.+0.5, -pixmap.height()/2.)
|
|
|
|
|
|
|
|
def updateItem(self):
|
|
|
|
symbolOpts = (self._data['pen'], self._data['brush'], self._data['size'], self._data['symbol'])
|
2012-03-18 18:57:36 +00:00
|
|
|
|
2012-05-11 03:37:07 +00:00
|
|
|
## If all symbol options are default, use default pixmap
|
|
|
|
if symbolOpts == (None, None, -1, ''):
|
|
|
|
pixmap = self._plot.defaultSpotPixmap()
|
2012-03-02 02:55:32 +00:00
|
|
|
else:
|
2012-05-11 03:37:07 +00:00
|
|
|
pixmap = makeSymbolPixmap(size=self.size(), pen=self.pen(), brush=self.brush(), symbol=self.symbol())
|
|
|
|
self.setPixmap(pixmap)
|
2012-03-02 02:55:32 +00:00
|
|
|
|
|
|
|
|
2012-05-11 03:37:07 +00:00
|
|
|
class PathSpotItem(SpotItem, QtGui.QGraphicsPathItem):
|
|
|
|
def __init__(self, data, plot):
|
|
|
|
QtGui.QGraphicsPathItem.__init__(self)
|
|
|
|
SpotItem.__init__(self, data, plot)
|
2012-03-02 02:55:32 +00:00
|
|
|
|
2012-05-11 03:37:07 +00:00
|
|
|
def updateItem(self):
|
|
|
|
QtGui.QGraphicsPathItem.setPath(self, Symbols[self.symbol()])
|
|
|
|
QtGui.QGraphicsPathItem.setPen(self, self.pen())
|
|
|
|
QtGui.QGraphicsPathItem.setBrush(self, self.brush())
|
|
|
|
size = self.size()
|
|
|
|
self.resetTransform()
|
|
|
|
self.scale(size, size)
|