new markers

This commit is contained in:
lesauxvi 2016-01-15 09:17:52 +01:00
parent 9e8c2082ed
commit 905a541253
2 changed files with 171 additions and 126 deletions

34
examples/Markers.py Executable file
View File

@ -0,0 +1,34 @@
# -*- coding: utf-8 -*-
"""
This example shows all the markers available into pyqtgraph.
"""
import initExample ## Add path to library (just for examples; you do not need this)
from pyqtgraph.Qt import QtGui, QtCore
import numpy as np
import pyqtgraph as pg
app = QtGui.QApplication([])
win = pg.GraphicsWindow(title="Pyqtgraph markers")
win.resize(1000,600)
pg.setConfigOptions(antialias=True)
plot = win.addPlot(title="Plotting with markers")
plot.plot([0, 1, 2, 3, 4], pen=(0,0,200), symbolBrush=(0,0,200), symbolPen='w', symbol='o')
plot.plot([1, 2, 3, 4, 5], pen=(0,128,0), symbolBrush=(0,128,0), symbolPen='w', symbol='t')
plot.plot([2, 3, 4, 5, 6], pen=(19,234,201), symbolBrush=(19,234,201), symbolPen='w', symbol='t1')
plot.plot([3, 4, 5, 6, 7], pen=(195,46,212), symbolBrush=(195,46,212), symbolPen='w', symbol='t2')
plot.plot([4, 5, 6, 7, 8], pen=(250,194,5), symbolBrush=(250,194,5), symbolPen='w', symbol='t3')
plot.plot([5, 6, 7, 8, 9], pen=(54,55,55), symbolBrush=(55,55,55), symbolPen='w', symbol='s')
plot.plot([6, 7, 8, 9, 10], pen=(0,114,189), symbolBrush=(0,114,189), symbolPen='w', symbol='p')
plot.plot([7, 8, 9, 10, 11], pen=(217,83,25), symbolBrush=(217,83,25), symbolPen='w', symbol='h')
plot.plot([8, 9, 10, 11, 12], pen=(237,177,32), symbolBrush=(237,177,32), symbolPen='w', symbol='star')
plot.plot([9, 10, 11, 12, 13], pen=(126,47,142), symbolBrush=(126,47,142), symbolPen='w', symbol='+')
plot.plot([10, 11, 12, 13, 14], pen=(119,172,48), symbolBrush=(119,172,48), symbolPen='w', symbol='d')
## 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

@ -19,17 +19,28 @@ __all__ = ['ScatterPlotItem', 'SpotItem']
## Build all symbol paths ## Build all symbol paths
Symbols = OrderedDict([(name, QtGui.QPainterPath()) for name in ['o', 's', 't', 'd', '+', 'x']]) Symbols = OrderedDict([(name, QtGui.QPainterPath()) for name in ['o', 's', 't', 't1', 't2', 't3','d', '+', 'x', 'p', 'h', 'star']])
Symbols['o'].addEllipse(QtCore.QRectF(-0.5, -0.5, 1, 1)) Symbols['o'].addEllipse(QtCore.QRectF(-0.5, -0.5, 1, 1))
Symbols['s'].addRect(QtCore.QRectF(-0.5, -0.5, 1, 1)) Symbols['s'].addRect(QtCore.QRectF(-0.5, -0.5, 1, 1))
coords = { coords = {
't': [(-0.5, -0.5), (0, 0.5), (0.5, -0.5)], 't': [(-0.5, -0.5), (0, 0.5), (0.5, -0.5)],
't1': [(-0.5, 0.5), (0, -0.5), (0.5, 0.5)],
't2': [(-0.5, -0.5), (-0.5, 0.5), (0.5, 0)],
't3': [(0.5, 0.5), (0.5, -0.5), (-0.5, 0)],
'd': [(0., -0.5), (-0.4, 0.), (0, 0.5), (0.4, 0)], '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.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.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) (0.05, -0.05), (0.05, -0.5), (-0.05, -0.5), (-0.05, -0.05)
], ],
'p': [(0, -0.5), (-0.4755, -0.1545), (-0.2939, 0.4045),
(0.2939, 0.4045), (0.4755, -0.1545)],
'h': [(0.433, 0.25), (0., 0.5), (-0.433, 0.25), (-0.433, -0.25),
(0, -0.5), (0.433, -0.25)],
'star': [(0, -0.5), (-0.1123, -0.1545), (-0.4755, -0.1545),
(-0.1816, 0.059), (-0.2939, 0.4045), (0, 0.1910),
(0.2939, 0.4045), (0.1816, 0.059), (0.4755, -0.1545),
(0.1123, -0.1545)]
} }
for k, c in coords.items(): for k, c in coords.items():
Symbols[k].moveTo(*c[0]) Symbols[k].moveTo(*c[0])
@ -40,7 +51,7 @@ tr = QtGui.QTransform()
tr.rotate(45) tr.rotate(45)
Symbols['x'] = tr.map(Symbols['+']) Symbols['x'] = tr.map(Symbols['+'])
def drawSymbol(painter, symbol, size, pen, brush): def drawSymbol(painter, symbol, size, pen, brush):
if symbol is None: if symbol is None:
return return
@ -53,13 +64,13 @@ def drawSymbol(painter, symbol, size, pen, brush):
symbol = list(Symbols.values())[symbol % len(Symbols)] symbol = list(Symbols.values())[symbol % len(Symbols)]
painter.drawPath(symbol) painter.drawPath(symbol)
def renderSymbol(symbol, size, pen, brush, device=None): def renderSymbol(symbol, size, pen, brush, device=None):
""" """
Render a symbol specification to QImage. Render a symbol specification to QImage.
Symbol may be either a QPainterPath or one of the keys in the Symbols dict. Symbol may be either a QPainterPath or one of the keys in the Symbols dict.
If *device* is None, a new QPixmap will be returned. Otherwise, If *device* is None, a new QPixmap will be returned. Otherwise,
the symbol will be rendered into the device specified (See QPainter documentation the symbol will be rendered into the device specified (See QPainter documentation
for more information). for more information).
""" """
## Render a spot with the given parameters to a pixmap ## Render a spot with the given parameters to a pixmap
@ -80,33 +91,33 @@ def makeSymbolPixmap(size, pen, brush, symbol):
## deprecated ## deprecated
img = renderSymbol(symbol, size, pen, brush) img = renderSymbol(symbol, size, pen, brush)
return QtGui.QPixmap(img) return QtGui.QPixmap(img)
class SymbolAtlas(object): class SymbolAtlas(object):
""" """
Used to efficiently construct a single QPixmap containing all rendered symbols Used to efficiently construct a single QPixmap containing all rendered symbols
for a ScatterPlotItem. This is required for fragment rendering. for a ScatterPlotItem. This is required for fragment rendering.
Use example: Use example:
atlas = SymbolAtlas() atlas = SymbolAtlas()
sc1 = atlas.getSymbolCoords('o', 5, QPen(..), QBrush(..)) sc1 = atlas.getSymbolCoords('o', 5, QPen(..), QBrush(..))
sc2 = atlas.getSymbolCoords('t', 10, QPen(..), QBrush(..)) sc2 = atlas.getSymbolCoords('t', 10, QPen(..), QBrush(..))
pm = atlas.getAtlas() pm = atlas.getAtlas()
""" """
def __init__(self): def __init__(self):
# symbol key : QRect(...) coordinates where symbol can be found in atlas. # symbol key : QRect(...) coordinates where symbol can be found in atlas.
# note that the coordinate list will always be the same list object as # note that the coordinate list will always be the same list object as
# long as the symbol is in the atlas, but the coordinates may # long as the symbol is in the atlas, but the coordinates may
# change if the atlas is rebuilt. # change if the atlas is rebuilt.
# weak value; if all external refs to this list disappear, # weak value; if all external refs to this list disappear,
# the symbol will be forgotten. # the symbol will be forgotten.
self.symbolMap = weakref.WeakValueDictionary() self.symbolMap = weakref.WeakValueDictionary()
self.atlasData = None # numpy array of atlas image self.atlasData = None # numpy array of atlas image
self.atlas = None # atlas as QPixmap self.atlas = None # atlas as QPixmap
self.atlasValid = False self.atlasValid = False
self.max_width=0 self.max_width=0
def getSymbolCoords(self, opts): def getSymbolCoords(self, opts):
""" """
Given a list of spot records, return an object representing the coordinates of that symbol within the atlas Given a list of spot records, return an object representing the coordinates of that symbol within the atlas
@ -131,7 +142,7 @@ class SymbolAtlas(object):
keyi = key keyi = key
sourceRecti = newRectSrc sourceRecti = newRectSrc
return sourceRect return sourceRect
def buildAtlas(self): def buildAtlas(self):
# get rendered array for all symbols, keep track of avg/max width # get rendered array for all symbols, keep track of avg/max width
rendered = {} rendered = {}
@ -150,7 +161,7 @@ class SymbolAtlas(object):
w = arr.shape[0] w = arr.shape[0]
avgWidth += w avgWidth += w
maxWidth = max(maxWidth, w) maxWidth = max(maxWidth, w)
nSymbols = len(rendered) nSymbols = len(rendered)
if nSymbols > 0: if nSymbols > 0:
avgWidth /= nSymbols avgWidth /= nSymbols
@ -158,10 +169,10 @@ class SymbolAtlas(object):
else: else:
avgWidth = 0 avgWidth = 0
width = 0 width = 0
# sort symbols by height # sort symbols by height
symbols = sorted(rendered.keys(), key=lambda x: rendered[x].shape[1], reverse=True) symbols = sorted(rendered.keys(), key=lambda x: rendered[x].shape[1], reverse=True)
self.atlasRows = [] self.atlasRows = []
x = width x = width
@ -187,7 +198,7 @@ class SymbolAtlas(object):
self.atlas = None self.atlas = None
self.atlasValid = True self.atlasValid = True
self.max_width = maxWidth self.max_width = maxWidth
def getAtlas(self): def getAtlas(self):
if not self.atlasValid: if not self.atlasValid:
self.buildAtlas() self.buildAtlas()
@ -197,27 +208,27 @@ class SymbolAtlas(object):
img = fn.makeQImage(self.atlasData, copy=False, transpose=False) img = fn.makeQImage(self.atlasData, copy=False, transpose=False)
self.atlas = QtGui.QPixmap(img) self.atlas = QtGui.QPixmap(img)
return self.atlas return self.atlas
class ScatterPlotItem(GraphicsObject): class ScatterPlotItem(GraphicsObject):
""" """
Displays a set of x/y points. Instances of this class are created Displays a set of x/y points. Instances of this class are created
automatically as part of PlotDataItem; these rarely need to be instantiated automatically as part of PlotDataItem; these rarely need to be instantiated
directly. directly.
The size, shape, pen, and fill brush may be set for each point individually The size, shape, pen, and fill brush may be set for each point individually
or for all points. or for all points.
======================== =============================================== ======================== ===============================================
**Signals:** **Signals:**
sigPlotChanged(self) Emitted when the data being plotted has changed sigPlotChanged(self) Emitted when the data being plotted has changed
sigClicked(self, points) Emitted when the curve is clicked. Sends a list sigClicked(self, points) Emitted when the curve is clicked. Sends a list
of all the points under the mouse pointer. of all the points under the mouse pointer.
======================== =============================================== ======================== ===============================================
""" """
#sigPointClicked = QtCore.Signal(object, object) #sigPointClicked = QtCore.Signal(object, object)
sigClicked = QtCore.Signal(object, object) ## self, points sigClicked = QtCore.Signal(object, object) ## self, points
@ -228,17 +239,17 @@ class ScatterPlotItem(GraphicsObject):
""" """
profiler = debug.Profiler() profiler = debug.Profiler()
GraphicsObject.__init__(self) GraphicsObject.__init__(self)
self.picture = None # QPicture used for rendering when pxmode==False self.picture = None # QPicture used for rendering when pxmode==False
self.fragmentAtlas = SymbolAtlas() self.fragmentAtlas = SymbolAtlas()
self.data = np.empty(0, dtype=[('x', float), ('y', float), ('size', float), ('symbol', object), ('pen', object), ('brush', object), ('data', object), ('item', object), ('sourceRect', object), ('targetRect', object), ('width', float)]) self.data = np.empty(0, dtype=[('x', float), ('y', float), ('size', float), ('symbol', object), ('pen', object), ('brush', object), ('data', object), ('item', object), ('sourceRect', object), ('targetRect', object), ('width', float)])
self.bounds = [None, None] ## caches data bounds self.bounds = [None, None] ## caches data bounds
self._maxSpotWidth = 0 ## maximum size of the scale-variant portion of all spots 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 self._maxSpotPxWidth = 0 ## maximum size of the scale-invariant portion of all spots
self.opts = { self.opts = {
'pxMode': True, 'pxMode': True,
'useCache': True, ## If useCache is False, symbols are re-drawn on every paint. 'useCache': True, ## If useCache is False, symbols are re-drawn on every paint.
'antialias': getConfigOption('antialias'), 'antialias': getConfigOption('antialias'),
'name': None, 'name': None,
} }
@ -252,14 +263,14 @@ class ScatterPlotItem(GraphicsObject):
profiler('setData') profiler('setData')
#self.setCacheMode(self.DeviceCoordinateCache) #self.setCacheMode(self.DeviceCoordinateCache)
def setData(self, *args, **kargs): def setData(self, *args, **kargs):
""" """
**Ordered Arguments:** **Ordered Arguments:**
* If there is only one unnamed argument, it will be interpreted like the 'spots' argument. * 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. * If there are two unnamed arguments, they will be interpreted as sequences of x and y values.
====================== =============================================================================================== ====================== ===============================================================================================
**Keyword Arguments:** **Keyword Arguments:**
*spots* Optional list of dicts. Each dict specifies parameters for a single spot: *spots* Optional list of dicts. Each dict specifies parameters for a single spot:
@ -285,8 +296,8 @@ class ScatterPlotItem(GraphicsObject):
it is in the item's local coordinate system. it is in the item's local coordinate system.
*data* a list of python objects used to uniquely identify each spot. *data* a list of python objects used to uniquely identify each spot.
*identical* *Deprecated*. This functionality is handled automatically now. *identical* *Deprecated*. This functionality is handled automatically now.
*antialias* Whether to draw symbols with antialiasing. Note that if pxMode is True, symbols are *antialias* Whether to draw symbols with antialiasing. Note that if pxMode is True, symbols are
always rendered with antialiasing (since the rendered symbols can be cached, this always rendered with antialiasing (since the rendered symbols can be cached, this
incurs very little performance cost) incurs very little performance cost)
*name* The name of this item. Names are used for automatically *name* The name of this item. Names are used for automatically
generating LegendItem entries and by some exporters. generating LegendItem entries and by some exporters.
@ -298,10 +309,10 @@ class ScatterPlotItem(GraphicsObject):
def addPoints(self, *args, **kargs): def addPoints(self, *args, **kargs):
""" """
Add new points to the scatter plot. Add new points to the scatter plot.
Arguments are the same as setData() Arguments are the same as setData()
""" """
## deal with non-keyword arguments ## deal with non-keyword arguments
if len(args) == 1: if len(args) == 1:
kargs['spots'] = args[0] kargs['spots'] = args[0]
@ -310,7 +321,7 @@ class ScatterPlotItem(GraphicsObject):
kargs['y'] = args[1] kargs['y'] = args[1]
elif len(args) > 2: elif len(args) > 2:
raise Exception('Only accepts up to two non-keyword arguments.') raise Exception('Only accepts up to two non-keyword arguments.')
## convert 'pos' argument to 'x' and 'y' ## convert 'pos' argument to 'x' and 'y'
if 'pos' in kargs: if 'pos' in kargs:
pos = kargs['pos'] pos = kargs['pos']
@ -329,7 +340,7 @@ class ScatterPlotItem(GraphicsObject):
y.append(p[1]) y.append(p[1])
kargs['x'] = x kargs['x'] = x
kargs['y'] = y kargs['y'] = y
## determine how many spots we have ## determine how many spots we have
if 'spots' in kargs: if 'spots' in kargs:
numPts = len(kargs['spots']) numPts = len(kargs['spots'])
@ -339,16 +350,16 @@ class ScatterPlotItem(GraphicsObject):
kargs['x'] = [] kargs['x'] = []
kargs['y'] = [] kargs['y'] = []
numPts = 0 numPts = 0
## Extend record array ## Extend record array
oldData = self.data oldData = self.data
self.data = np.empty(len(oldData)+numPts, dtype=self.data.dtype) self.data = np.empty(len(oldData)+numPts, dtype=self.data.dtype)
## note that np.empty initializes object fields to None and string fields to '' ## note that np.empty initializes object fields to None and string fields to ''
self.data[:len(oldData)] = oldData self.data[:len(oldData)] = oldData
#for i in range(len(oldData)): #for i in range(len(oldData)):
#oldData[i]['item']._data = self.data[i] ## Make sure items have proper reference to new array #oldData[i]['item']._data = self.data[i] ## Make sure items have proper reference to new array
newData = self.data[len(oldData):] newData = self.data[len(oldData):]
newData['size'] = -1 ## indicates to use default size newData['size'] = -1 ## indicates to use default size
@ -376,12 +387,12 @@ class ScatterPlotItem(GraphicsObject):
elif 'y' in kargs: elif 'y' in kargs:
newData['x'] = kargs['x'] newData['x'] = kargs['x']
newData['y'] = kargs['y'] newData['y'] = kargs['y']
if 'pxMode' in kargs: if 'pxMode' in kargs:
self.setPxMode(kargs['pxMode']) self.setPxMode(kargs['pxMode'])
if 'antialias' in kargs: if 'antialias' in kargs:
self.opts['antialias'] = kargs['antialias'] self.opts['antialias'] = kargs['antialias']
## Set any extra parameters provided in keyword arguments ## Set any extra parameters provided in keyword arguments
for k in ['pen', 'brush', 'symbol', 'size']: for k in ['pen', 'brush', 'symbol', 'size']:
if k in kargs: if k in kargs:
@ -397,32 +408,32 @@ class ScatterPlotItem(GraphicsObject):
self.invalidate() self.invalidate()
self.updateSpots(newData) self.updateSpots(newData)
self.sigPlotChanged.emit(self) self.sigPlotChanged.emit(self)
def invalidate(self): def invalidate(self):
## clear any cached drawing state ## clear any cached drawing state
self.picture = None self.picture = None
self.update() self.update()
def getData(self): def getData(self):
return self.data['x'], self.data['y'] return self.data['x'], self.data['y']
def setPoints(self, *args, **kargs): def setPoints(self, *args, **kargs):
##Deprecated; use setData ##Deprecated; use setData
return self.setData(*args, **kargs) return self.setData(*args, **kargs)
def implements(self, interface=None): def implements(self, interface=None):
ints = ['plotData'] ints = ['plotData']
if interface is None: if interface is None:
return ints return ints
return interface in ints return interface in ints
def name(self): def name(self):
return self.opts.get('name', None) return self.opts.get('name', None)
def setPen(self, *args, **kargs): def setPen(self, *args, **kargs):
"""Set the pen(s) used to draw the outline around each spot. """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. 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 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.""" all spots which do not have a pen explicitly set."""
update = kargs.pop('update', True) update = kargs.pop('update', True)
dataSet = kargs.pop('dataSet', self.data) dataSet = kargs.pop('dataSet', self.data)
@ -436,19 +447,19 @@ class ScatterPlotItem(GraphicsObject):
dataSet['pen'] = pens dataSet['pen'] = pens
else: else:
self.opts['pen'] = fn.mkPen(*args, **kargs) self.opts['pen'] = fn.mkPen(*args, **kargs)
dataSet['sourceRect'] = None dataSet['sourceRect'] = None
if update: if update:
self.updateSpots(dataSet) self.updateSpots(dataSet)
def setBrush(self, *args, **kargs): def setBrush(self, *args, **kargs):
"""Set the brush(es) used to fill the interior of each spot. """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. 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 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.""" all spots which do not have a brush explicitly set."""
update = kargs.pop('update', True) update = kargs.pop('update', True)
dataSet = kargs.pop('dataSet', self.data) dataSet = kargs.pop('dataSet', self.data)
if len(args) == 1 and (isinstance(args[0], np.ndarray) or isinstance(args[0], list)): if len(args) == 1 and (isinstance(args[0], np.ndarray) or isinstance(args[0], list)):
brushes = args[0] brushes = args[0]
if 'mask' in kargs and kargs['mask'] is not None: if 'mask' in kargs and kargs['mask'] is not None:
@ -459,19 +470,19 @@ class ScatterPlotItem(GraphicsObject):
else: else:
self.opts['brush'] = fn.mkBrush(*args, **kargs) self.opts['brush'] = fn.mkBrush(*args, **kargs)
#self._spotPixmap = None #self._spotPixmap = None
dataSet['sourceRect'] = None dataSet['sourceRect'] = None
if update: if update:
self.updateSpots(dataSet) self.updateSpots(dataSet)
def setSymbol(self, symbol, update=True, dataSet=None, mask=None): def setSymbol(self, symbol, update=True, dataSet=None, mask=None):
"""Set the symbol(s) used to draw each spot. """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. 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 Otherwise, the argument will be used as the default symbol for
all spots which do not have a symbol explicitly set.""" all spots which do not have a symbol explicitly set."""
if dataSet is None: if dataSet is None:
dataSet = self.data dataSet = self.data
if isinstance(symbol, np.ndarray) or isinstance(symbol, list): if isinstance(symbol, np.ndarray) or isinstance(symbol, list):
symbols = symbol symbols = symbol
if mask is not None: if mask is not None:
@ -482,19 +493,19 @@ class ScatterPlotItem(GraphicsObject):
else: else:
self.opts['symbol'] = symbol self.opts['symbol'] = symbol
self._spotPixmap = None self._spotPixmap = None
dataSet['sourceRect'] = None dataSet['sourceRect'] = None
if update: if update:
self.updateSpots(dataSet) self.updateSpots(dataSet)
def setSize(self, size, update=True, dataSet=None, mask=None): def setSize(self, size, update=True, dataSet=None, mask=None):
"""Set the size(s) used to draw each spot. """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. 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 Otherwise, the argument will be used as the default size for
all spots which do not have a size explicitly set.""" all spots which do not have a size explicitly set."""
if dataSet is None: if dataSet is None:
dataSet = self.data dataSet = self.data
if isinstance(size, np.ndarray) or isinstance(size, list): if isinstance(size, np.ndarray) or isinstance(size, list):
sizes = size sizes = size
if mask is not None: if mask is not None:
@ -505,21 +516,21 @@ class ScatterPlotItem(GraphicsObject):
else: else:
self.opts['size'] = size self.opts['size'] = size
self._spotPixmap = None self._spotPixmap = None
dataSet['sourceRect'] = None dataSet['sourceRect'] = None
if update: if update:
self.updateSpots(dataSet) self.updateSpots(dataSet)
def setPointData(self, data, dataSet=None, mask=None): def setPointData(self, data, dataSet=None, mask=None):
if dataSet is None: if dataSet is None:
dataSet = self.data dataSet = self.data
if isinstance(data, np.ndarray) or isinstance(data, list): if isinstance(data, np.ndarray) or isinstance(data, list):
if mask is not None: if mask is not None:
data = data[mask] data = data[mask]
if len(data) != len(dataSet): if len(data) != len(dataSet):
raise Exception("Length of meta data does not match number of points (%d != %d)" % (len(data), len(dataSet))) raise Exception("Length of meta data does not match number of points (%d != %d)" % (len(data), len(dataSet)))
## Bug: If data is a numpy record array, then items from that array must be copied to dataSet one at a time. ## 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. ## (otherwise they are converted to tuples and thus lose their field names.
if isinstance(data, np.ndarray) and (data.dtype.fields is not None)and len(data.dtype.fields) > 1: if isinstance(data, np.ndarray) and (data.dtype.fields is not None)and len(data.dtype.fields) > 1:
@ -527,14 +538,14 @@ class ScatterPlotItem(GraphicsObject):
dataSet['data'][i] = rec dataSet['data'][i] = rec
else: else:
dataSet['data'] = data dataSet['data'] = data
def setPxMode(self, mode): def setPxMode(self, mode):
if self.opts['pxMode'] == mode: if self.opts['pxMode'] == mode:
return return
self.opts['pxMode'] = mode self.opts['pxMode'] = mode
self.invalidate() self.invalidate()
def updateSpots(self, dataSet=None): def updateSpots(self, dataSet=None):
if dataSet is None: if dataSet is None:
dataSet = self.data dataSet = self.data
@ -547,9 +558,9 @@ class ScatterPlotItem(GraphicsObject):
opts = self.getSpotOpts(dataSet[mask]) opts = self.getSpotOpts(dataSet[mask])
sourceRect = self.fragmentAtlas.getSymbolCoords(opts) sourceRect = self.fragmentAtlas.getSymbolCoords(opts)
dataSet['sourceRect'][mask] = sourceRect dataSet['sourceRect'][mask] = sourceRect
self.fragmentAtlas.getAtlas() # generate atlas so source widths are available. self.fragmentAtlas.getAtlas() # generate atlas so source widths are available.
dataSet['width'] = np.array(list(imap(QtCore.QRectF.width, dataSet['sourceRect'])))/2 dataSet['width'] = np.array(list(imap(QtCore.QRectF.width, dataSet['sourceRect'])))/2
dataSet['targetRect'] = None dataSet['targetRect'] = None
self._maxSpotPxWidth = self.fragmentAtlas.max_width self._maxSpotPxWidth = self.fragmentAtlas.max_width
@ -585,9 +596,9 @@ class ScatterPlotItem(GraphicsObject):
recs['pen'][np.equal(recs['pen'], None)] = fn.mkPen(self.opts['pen']) recs['pen'][np.equal(recs['pen'], None)] = fn.mkPen(self.opts['pen'])
recs['brush'][np.equal(recs['brush'], None)] = fn.mkBrush(self.opts['brush']) recs['brush'][np.equal(recs['brush'], None)] = fn.mkBrush(self.opts['brush'])
return recs return recs
def measureSpotSizes(self, dataSet): def measureSpotSizes(self, dataSet):
for rec in dataSet: for rec in dataSet:
## keep track of the maximum spot size and pixel size ## keep track of the maximum spot size and pixel size
@ -605,8 +616,8 @@ class ScatterPlotItem(GraphicsObject):
self._maxSpotWidth = max(self._maxSpotWidth, width) self._maxSpotWidth = max(self._maxSpotWidth, width)
self._maxSpotPxWidth = max(self._maxSpotPxWidth, pxWidth) self._maxSpotPxWidth = max(self._maxSpotPxWidth, pxWidth)
self.bounds = [None, None] self.bounds = [None, None]
def clear(self): def clear(self):
"""Remove all spots from the scatter plot""" """Remove all spots from the scatter plot"""
#self.clearItems() #self.clearItems()
@ -617,23 +628,23 @@ class ScatterPlotItem(GraphicsObject):
def dataBounds(self, ax, frac=1.0, orthoRange=None): def dataBounds(self, ax, frac=1.0, orthoRange=None):
if frac >= 1.0 and orthoRange is None and self.bounds[ax] is not None: if frac >= 1.0 and orthoRange is None and self.bounds[ax] is not None:
return self.bounds[ax] return self.bounds[ax]
#self.prepareGeometryChange() #self.prepareGeometryChange()
if self.data is None or len(self.data) == 0: if self.data is None or len(self.data) == 0:
return (None, None) return (None, None)
if ax == 0: if ax == 0:
d = self.data['x'] d = self.data['x']
d2 = self.data['y'] d2 = self.data['y']
elif ax == 1: elif ax == 1:
d = self.data['y'] d = self.data['y']
d2 = self.data['x'] d2 = self.data['x']
if orthoRange is not None: if orthoRange is not None:
mask = (d2 >= orthoRange[0]) * (d2 <= orthoRange[1]) mask = (d2 >= orthoRange[0]) * (d2 <= orthoRange[1])
d = d[mask] d = d[mask]
d2 = d2[mask] d2 = d2[mask]
if frac >= 1.0: if frac >= 1.0:
self.bounds[ax] = (np.nanmin(d) - self._maxSpotWidth*0.7072, np.nanmax(d) + self._maxSpotWidth*0.7072) self.bounds[ax] = (np.nanmin(d) - self._maxSpotWidth*0.7072, np.nanmax(d) + self._maxSpotWidth*0.7072)
return self.bounds[ax] return self.bounds[ax]
@ -656,11 +667,11 @@ class ScatterPlotItem(GraphicsObject):
if ymn is None or ymx is None: if ymn is None or ymx is None:
ymn = 0 ymn = 0
ymx = 0 ymx = 0
px = py = 0.0 px = py = 0.0
pxPad = self.pixelPadding() pxPad = self.pixelPadding()
if pxPad > 0: if pxPad > 0:
# determine length of pixel in local x, y directions # determine length of pixel in local x, y directions
px, py = self.pixelVectors() px, py = self.pixelVectors()
try: try:
px = 0 if px is None else px.length() px = 0 if px is None else px.length()
@ -670,7 +681,7 @@ class ScatterPlotItem(GraphicsObject):
py = 0 if py is None else py.length() py = 0 if py is None else py.length()
except OverflowError: except OverflowError:
py = 0 py = 0
# return bounds expanded by pixel size # return bounds expanded by pixel size
px *= pxPad px *= pxPad
py *= pxPad py *= pxPad
@ -688,7 +699,7 @@ class ScatterPlotItem(GraphicsObject):
def mapPointsToDevice(self, pts): def mapPointsToDevice(self, pts):
# Map point locations to device # Map point locations to device
tr = self.deviceTransform() tr = self.deviceTransform()
if tr is None: if tr is None:
return None return None
@ -699,7 +710,7 @@ class ScatterPlotItem(GraphicsObject):
pts = fn.transformCoordinates(tr, pts) pts = fn.transformCoordinates(tr, pts)
pts -= self.data['width'] pts -= self.data['width']
pts = np.clip(pts, -2**30, 2**30) ## prevent Qt segmentation fault. pts = np.clip(pts, -2**30, 2**30) ## prevent Qt segmentation fault.
return pts return pts
def getViewMask(self, pts): def getViewMask(self, pts):
@ -713,48 +724,48 @@ class ScatterPlotItem(GraphicsObject):
mask = ((pts[0] + w > viewBounds.left()) & mask = ((pts[0] + w > viewBounds.left()) &
(pts[0] - w < viewBounds.right()) & (pts[0] - w < viewBounds.right()) &
(pts[1] + w > viewBounds.top()) & (pts[1] + w > viewBounds.top()) &
(pts[1] - w < viewBounds.bottom())) ## remove out of view points (pts[1] - w < viewBounds.bottom())) ## remove out of view points
return mask return mask
@debug.warnOnException ## raising an exception here causes crash @debug.warnOnException ## raising an exception here causes crash
def paint(self, p, *args): def paint(self, p, *args):
#p.setPen(fn.mkPen('r')) #p.setPen(fn.mkPen('r'))
#p.drawRect(self.boundingRect()) #p.drawRect(self.boundingRect())
if self._exportOpts is not False: if self._exportOpts is not False:
aa = self._exportOpts.get('antialias', True) aa = self._exportOpts.get('antialias', True)
scale = self._exportOpts.get('resolutionScale', 1.0) ## exporting to image; pixel resolution may have changed scale = self._exportOpts.get('resolutionScale', 1.0) ## exporting to image; pixel resolution may have changed
else: else:
aa = self.opts['antialias'] aa = self.opts['antialias']
scale = 1.0 scale = 1.0
if self.opts['pxMode'] is True: if self.opts['pxMode'] is True:
p.resetTransform() p.resetTransform()
# Map point coordinates to device # Map point coordinates to device
pts = np.vstack([self.data['x'], self.data['y']]) pts = np.vstack([self.data['x'], self.data['y']])
pts = self.mapPointsToDevice(pts) pts = self.mapPointsToDevice(pts)
if pts is None: if pts is None:
return return
# Cull points that are outside view # Cull points that are outside view
viewMask = self.getViewMask(pts) viewMask = self.getViewMask(pts)
#pts = pts[:,mask] #pts = pts[:,mask]
#data = self.data[mask] #data = self.data[mask]
if self.opts['useCache'] and self._exportOpts is False: if self.opts['useCache'] and self._exportOpts is False:
# Draw symbols from pre-rendered atlas # Draw symbols from pre-rendered atlas
atlas = self.fragmentAtlas.getAtlas() atlas = self.fragmentAtlas.getAtlas()
# Update targetRects if necessary # Update targetRects if necessary
updateMask = viewMask & np.equal(self.data['targetRect'], None) updateMask = viewMask & np.equal(self.data['targetRect'], None)
if np.any(updateMask): if np.any(updateMask):
updatePts = pts[:,updateMask] updatePts = pts[:,updateMask]
width = self.data[updateMask]['width']*2 width = self.data[updateMask]['width']*2
self.data['targetRect'][updateMask] = list(imap(QtCore.QRectF, updatePts[0,:], updatePts[1,:], width, width)) self.data['targetRect'][updateMask] = list(imap(QtCore.QRectF, updatePts[0,:], updatePts[1,:], width, width))
data = self.data[viewMask] data = self.data[viewMask]
if USE_PYSIDE or USE_PYQT5: if USE_PYSIDE or USE_PYQT5:
list(imap(p.drawPixmap, data['targetRect'], repeat(atlas), data['sourceRect'])) list(imap(p.drawPixmap, data['targetRect'], repeat(atlas), data['sourceRect']))
@ -782,16 +793,16 @@ class ScatterPlotItem(GraphicsObject):
p2.translate(rec['x'], rec['y']) p2.translate(rec['x'], rec['y'])
drawSymbol(p2, *self.getSpotOpts(rec, scale)) drawSymbol(p2, *self.getSpotOpts(rec, scale))
p2.end() p2.end()
p.setRenderHint(p.Antialiasing, aa) p.setRenderHint(p.Antialiasing, aa)
self.picture.play(p) self.picture.play(p)
def points(self): def points(self):
for rec in self.data: for rec in self.data:
if rec['item'] is None: if rec['item'] is None:
rec['item'] = SpotItem(rec, self) rec['item'] = SpotItem(rec, self)
return self.data['item'] return self.data['item']
def pointsAt(self, pos): def pointsAt(self, pos):
x = pos.x() x = pos.x()
y = pos.y() y = pos.y()
@ -814,7 +825,7 @@ class ScatterPlotItem(GraphicsObject):
#print "No hit:", (x, y), (sx, sy) #print "No hit:", (x, y), (sx, sy)
#print " ", (sx-s2x, sy-s2y), (sx+s2x, sy+s2y) #print " ", (sx-s2x, sy-s2y), (sx+s2x, sy+s2y)
return pts[::-1] return pts[::-1]
def mouseClickEvent(self, ev): def mouseClickEvent(self, ev):
if ev.button() == QtCore.Qt.LeftButton: if ev.button() == QtCore.Qt.LeftButton:
@ -833,7 +844,7 @@ class ScatterPlotItem(GraphicsObject):
class SpotItem(object): class SpotItem(object):
""" """
Class referring to individual spots in a scatter plot. Class referring to individual spots in a scatter plot.
These can be retrieved by calling ScatterPlotItem.points() or These can be retrieved by calling ScatterPlotItem.points() or
by connecting to the ScatterPlotItem's click signals. by connecting to the ScatterPlotItem's click signals.
""" """
@ -844,34 +855,34 @@ class SpotItem(object):
#self.setParentItem(plot) #self.setParentItem(plot)
#self.setPos(QtCore.QPointF(data['x'], data['y'])) #self.setPos(QtCore.QPointF(data['x'], data['y']))
#self.updateItem() #self.updateItem()
def data(self): def data(self):
"""Return the user data associated with this spot.""" """Return the user data associated with this spot."""
return self._data['data'] return self._data['data']
def size(self): def size(self):
"""Return the size of this spot. """Return the size of this spot.
If the spot has no explicit size set, then return the ScatterPlotItem's default size instead.""" If the spot has no explicit size set, then return the ScatterPlotItem's default size instead."""
if self._data['size'] == -1: if self._data['size'] == -1:
return self._plot.opts['size'] return self._plot.opts['size']
else: else:
return self._data['size'] return self._data['size']
def pos(self): def pos(self):
return Point(self._data['x'], self._data['y']) return Point(self._data['x'], self._data['y'])
def viewPos(self): def viewPos(self):
return self._plot.mapToView(self.pos()) return self._plot.mapToView(self.pos())
def setSize(self, size): def setSize(self, size):
"""Set the size of this spot. """Set the size of this spot.
If the size is set to -1, then the ScatterPlotItem's default size If the size is set to -1, then the ScatterPlotItem's default size
will be used instead.""" will be used instead."""
self._data['size'] = size self._data['size'] = size
self.updateItem() self.updateItem()
def symbol(self): def symbol(self):
"""Return the symbol of this spot. """Return the symbol of this spot.
If the spot has no explicit symbol set, then return the ScatterPlotItem's default symbol instead. If the spot has no explicit symbol set, then return the ScatterPlotItem's default symbol instead.
""" """
symbol = self._data['symbol'] symbol = self._data['symbol']
@ -883,7 +894,7 @@ class SpotItem(object):
except: except:
pass pass
return symbol return symbol
def setSymbol(self, symbol): def setSymbol(self, symbol):
"""Set the symbol for this spot. """Set the symbol for this spot.
If the symbol is set to '', then the ScatterPlotItem's default symbol will be used instead.""" If the symbol is set to '', then the ScatterPlotItem's default symbol will be used instead."""
@ -895,35 +906,35 @@ class SpotItem(object):
if pen is None: if pen is None:
pen = self._plot.opts['pen'] pen = self._plot.opts['pen']
return fn.mkPen(pen) return fn.mkPen(pen)
def setPen(self, *args, **kargs): def setPen(self, *args, **kargs):
"""Set the outline pen for this spot""" """Set the outline pen for this spot"""
pen = fn.mkPen(*args, **kargs) pen = fn.mkPen(*args, **kargs)
self._data['pen'] = pen self._data['pen'] = pen
self.updateItem() self.updateItem()
def resetPen(self): def resetPen(self):
"""Remove the pen set for this spot; the scatter plot's default pen will be used instead.""" """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._data['pen'] = None ## Note this is NOT the same as calling setPen(None)
self.updateItem() self.updateItem()
def brush(self): def brush(self):
brush = self._data['brush'] brush = self._data['brush']
if brush is None: if brush is None:
brush = self._plot.opts['brush'] brush = self._plot.opts['brush']
return fn.mkBrush(brush) return fn.mkBrush(brush)
def setBrush(self, *args, **kargs): def setBrush(self, *args, **kargs):
"""Set the fill brush for this spot""" """Set the fill brush for this spot"""
brush = fn.mkBrush(*args, **kargs) brush = fn.mkBrush(*args, **kargs)
self._data['brush'] = brush self._data['brush'] = brush
self.updateItem() self.updateItem()
def resetBrush(self): def resetBrush(self):
"""Remove the brush set for this spot; the scatter plot's default brush will be used instead.""" """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._data['brush'] = None ## Note this is NOT the same as calling setBrush(None)
self.updateItem() self.updateItem()
def setData(self, data): def setData(self, data):
"""Set the user-data associated with this spot""" """Set the user-data associated with this spot"""
self._data['data'] = data self._data['data'] = data
@ -938,14 +949,14 @@ class SpotItem(object):
#QtGui.QGraphicsPixmapItem.__init__(self) #QtGui.QGraphicsPixmapItem.__init__(self)
#self.setFlags(self.flags() | self.ItemIgnoresTransformations) #self.setFlags(self.flags() | self.ItemIgnoresTransformations)
#SpotItem.__init__(self, data, plot) #SpotItem.__init__(self, data, plot)
#def setPixmap(self, pixmap): #def setPixmap(self, pixmap):
#QtGui.QGraphicsPixmapItem.setPixmap(self, pixmap) #QtGui.QGraphicsPixmapItem.setPixmap(self, pixmap)
#self.setOffset(-pixmap.width()/2.+0.5, -pixmap.height()/2.) #self.setOffset(-pixmap.width()/2.+0.5, -pixmap.height()/2.)
#def updateItem(self): #def updateItem(self):
#symbolOpts = (self._data['pen'], self._data['brush'], self._data['size'], self._data['symbol']) #symbolOpts = (self._data['pen'], self._data['brush'], self._data['size'], self._data['symbol'])
### If all symbol options are default, use default pixmap ### If all symbol options are default, use default pixmap
#if symbolOpts == (None, None, -1, ''): #if symbolOpts == (None, None, -1, ''):
#pixmap = self._plot.defaultSpotPixmap() #pixmap = self._plot.defaultSpotPixmap()