ScatterPlotItem.py 22.5 KB
Newer Older
1
2
3
from pyqtgraph.Qt import QtGui, QtCore
from pyqtgraph.Point import Point
import pyqtgraph.functions as fn
Luke Campagnola's avatar
Luke Campagnola committed
4
from GraphicsItem import GraphicsItem
5
from GraphicsObject import GraphicsObject
6
7
import numpy as np
import scipy.stats
Luke Campagnola's avatar
Luke Campagnola committed
8
9
10
11
import weakref
import pyqtgraph.debug as debug
from collections import OrderedDict
#import pyqtgraph as pg 
12
13

__all__ = ['ScatterPlotItem', 'SpotItem']
14
15
16


## Build all symbol paths
Luke Campagnola's avatar
Luke Campagnola committed
17
Symbols = OrderedDict([(name, QtGui.QPainterPath()) for name in ['o', 's', 't', 'd', '+']])
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
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)
    ],
}
for k, c in coords.iteritems():
    Symbols[k].moveTo(*c[0])
    for x,y in c[1:]:
        Symbols[k].lineTo(x, y)
    Symbols[k].closeSubpath()


Luke Campagnola's avatar
Luke Campagnola committed
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
def makeSymbolPixmap(size, pen, brush, symbol):
    ## Render a spot with the given parameters to a pixmap
    image = QtGui.QImage(size+2, size+2, QtGui.QImage.Format_ARGB32_Premultiplied)
    image.fill(0)
    p = QtGui.QPainter(image)
    p.setRenderHint(p.Antialiasing)
    p.translate(size*0.5+1, size*0.5+1)
    p.scale(size, size)
    p.setPen(pen)
    p.setBrush(brush)
    p.drawPath(Symbols[symbol])
    p.end()
    return QtGui.QPixmap(image)


51

52
class ScatterPlotItem(GraphicsObject):
53
54
55
56
    """
    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.
57
    
58
59
60
61
62
63
64
65
66
67
68
69
    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.
    ========================  ===============================================
    
    """
70
71
72
73
    #sigPointClicked = QtCore.Signal(object, object)
    sigClicked = QtCore.Signal(object, object)  ## self, points
    sigPlotChanged = QtCore.Signal(object)
    
74
75
76
77
    def __init__(self, *args, **kargs):
        """
        Accepts the same arguments as setData()
        """
Luke Campagnola's avatar
Luke Campagnola committed
78
        prof = debug.Profiler('ScatterPlotItem.__init__', disabled=True)
79
        GraphicsObject.__init__(self)
Luke Campagnola's avatar
Luke Campagnola committed
80
81
82
        self.setFlag(self.ItemHasNoContents, True)
        self.data = np.empty(0, dtype=[('x', float), ('y', float), ('size', float), ('symbol', 'S1'), ('pen', object), ('brush', object), ('item', object), ('data', object)])
        #self.spots = []
83
        #self.fragments = None
84
        self.bounds = [None, None]
Luke Campagnola's avatar
Luke Campagnola committed
85
86
87
        self.opts = {'pxMode': True}
        #self.spotsValid = False
        #self.itemsValid = False
88
89
        self._spotPixmap = None
        
Luke Campagnola's avatar
Luke Campagnola committed
90
91
92
93
94
95
        self.setPen(200,200,200, update=False)
        self.setBrush(100,100,150, update=False)
        self.setSymbol('o', update=False)
        self.setSize(7, update=False)
        #self.setIdentical(False, update=False)
        prof.mark('1')
96
        self.setData(*args, **kargs)
Luke Campagnola's avatar
Luke Campagnola committed
97
98
        prof.mark('setData')
        prof.finish()
99
100
        
    def setData(self, *args, **kargs):
101
        """
102
103
104
105
106
        **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.
        
107
        ====================== ===============================================================================================
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
        **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.
Luke Campagnola's avatar
Luke Campagnola committed
129
        *identical*            *Deprecated*. This functionality is handled automatically now.
130
        ====================== ===============================================================================================
131
        """
Luke Campagnola's avatar
Luke Campagnola committed
132
133
        self.clear()  ## clear out all old data
        self.addPoints(*args, **kargs)
134

Luke Campagnola's avatar
Luke Campagnola committed
135
136
137
138
139
    def addPoints(self, *args, **kargs):
        """
        Add new points to the scatter plot. 
        Arguments are the same as setData()
        """
140
        
141
142
143
144
145
146
147
148
        ## 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.')
149
        
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
        ## 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
168
        
169
170
171
172
173
        ## 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'])
174
        else:
175
176
177
            kargs['x'] = []
            kargs['y'] = []
            numPts = 0
178
        
Luke Campagnola's avatar
Luke Campagnola committed
179
180
181
182
183
184
185
186
187
188
189
        ## 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
190
        
191
192
193
194
195
        if 'spots' in kargs:
            spots = kargs['spots']
            for i in xrange(len(spots)):
                spot = spots[i]
                for k in spot:
Luke Campagnola's avatar
Luke Campagnola committed
196
197
198
199
200
                    #if k == 'pen':
                        #newData[k] = fn.mkPen(spot[k])
                    #elif k == 'brush':
                        #newData[k] = fn.mkBrush(spot[k])
                    if k == 'pos':
201
202
203
204
205
                        pos = spot[k]
                        if isinstance(pos, QtCore.QPointF):
                            x,y = pos.x(), pos.y()
                        else:
                            x,y = pos[0], pos[1]
Luke Campagnola's avatar
Luke Campagnola committed
206
207
208
209
210
211
                        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]
212
213
214
                    else:
                        raise Exception("Unknown spot parameter: %s" % k)
        elif 'y' in kargs:
Luke Campagnola's avatar
Luke Campagnola committed
215
216
            newData['x'] = kargs['x']
            newData['y'] = kargs['y']
217
        
Luke Campagnola's avatar
Luke Campagnola committed
218
219
220
        if 'pxMode' in kargs:
            self.setPxMode(kargs['pxMode'], update=False)
            
221
        ## Set any extra parameters provided in keyword arguments
Luke Campagnola's avatar
Luke Campagnola committed
222
        for k in ['pen', 'brush', 'symbol', 'size']:
223
224
            if k in kargs:
                setMethod = getattr(self, 'set' + k[0].upper() + k[1:])
Luke Campagnola's avatar
Luke Campagnola committed
225
                setMethod(kargs[k], update=False, dataSet=newData)
226
        
Luke Campagnola's avatar
Luke Campagnola committed
227
228
        if 'data' in kargs:
            self.setPointData(kargs['data'], dataSet=newData)
229
        
Luke Campagnola's avatar
Luke Campagnola committed
230
231
232
        #self.updateSpots()
        self.generateSpotItems()
        self.sigPlotChanged.emit(self)
233
234
235
        
        
    def setPoints(self, *args, **kargs):
236
        ##Deprecated; use setData
237
238
        return self.setData(*args, **kargs)
        
239
240
241
242
243
244
    def implements(self, interface=None):
        ints = ['plotData']
        if interface is None:
            return ints
        return interface in ints
    
245
    def setPen(self, *args, **kargs):
Luke Campagnola's avatar
Luke Campagnola committed
246
247
248
249
250
251
252
        """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)
        
253
254
        if len(args) == 1 and (isinstance(args[0], np.ndarray) or isinstance(args[0], list)):
            pens = args[0]
Luke Campagnola's avatar
Luke Campagnola committed
255
256
257
            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
258
259
        else:
            self.opts['pen'] = fn.mkPen(*args, **kargs)
Luke Campagnola's avatar
Luke Campagnola committed
260
261
262
263
            self._spotPixmap = None
        
        if update:
            self.updateSpots(dataSet)
264
265
        
    def setBrush(self, *args, **kargs):
Luke Campagnola's avatar
Luke Campagnola committed
266
267
268
269
270
271
272
        """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)
            
273
274
        if len(args) == 1 and (isinstance(args[0], np.ndarray) or isinstance(args[0], list)):
            brushes = args[0]
Luke Campagnola's avatar
Luke Campagnola committed
275
276
277
278
279
            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
280
281
        else:
            self.opts['brush'] = fn.mkBrush(*args, **kargs)
Luke Campagnola's avatar
Luke Campagnola committed
282
283
284
285
            self._spotPixmap = None
        
        if update:
            self.updateSpots(dataSet)
286

Luke Campagnola's avatar
Luke Campagnola committed
287
288
289
290
291
292
293
294
    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
            
295
296
        if isinstance(symbol, np.ndarray) or isinstance(symbol, list):
            symbols = symbol
Luke Campagnola's avatar
Luke Campagnola committed
297
298
299
            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
300
301
        else:
            self.opts['symbol'] = symbol
Luke Campagnola's avatar
Luke Campagnola committed
302
            self._spotPixmap = None
303
        
Luke Campagnola's avatar
Luke Campagnola committed
304
305
306
307
308
309
310
311
312
313
314
        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
            
315
316
        if isinstance(size, np.ndarray) or isinstance(size, list):
            sizes = size
Luke Campagnola's avatar
Luke Campagnola committed
317
318
319
            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
320
321
        else:
            self.opts['size'] = size
Luke Campagnola's avatar
Luke Campagnola committed
322
323
324
325
            self._spotPixmap = None
            
        if update:
            self.updateSpots(dataSet)
326
        
Luke Campagnola's avatar
Luke Campagnola committed
327
328
329
330
    def setPointData(self, data, dataSet=None):
        if dataSet is None:
            dataSet = self.data
            
331
        if isinstance(data, np.ndarray) or isinstance(data, list):
Luke Campagnola's avatar
Luke Campagnola committed
332
333
334
            if len(data) != len(dataSet):
                raise Exception("Length of meta data does not match number of points (%d != %d)" % (len(data), len(dataSet)))
        dataSet['data'] = data
335
        
Luke Campagnola's avatar
Luke Campagnola committed
336
337
338
339
    def setPxMode(self, mode, update=True):
        if self.opts['pxMode'] == mode:
            return
            
340
        self.opts['pxMode'] = mode
Luke Campagnola's avatar
Luke Campagnola committed
341
342
343
        self.clearItems()
        if update:
            self.generateSpotItems()
344
        
Luke Campagnola's avatar
Luke Campagnola committed
345
346
347
348
349
    def updateSpots(self, dataSet=None):
        if dataSet is None:
            dataSet = self.data
        for spot in dataSet['item']:
            spot.updateItem()
350
        
351
    def clear(self):
Luke Campagnola's avatar
Luke Campagnola committed
352
353
354
355
356
357
358
359
360
        """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
361
362
363
364
            i.setParentItem(None)
            s = i.scene()
            if s is not None:
                s.removeItem(i)
Luke Campagnola's avatar
Luke Campagnola committed
365
        self.data['item'] = None
366
        
367
    def dataBounds(self, ax, frac=1.0, orthoRange=None):
368
369
        if frac >= 1.0 and self.bounds[ax] is not None:
            return self.bounds[ax]
370
        
371
372
373
374
375
        if self.data is None or len(self.data) == 0:
            return (None, None)
        
        if ax == 0:
            d = self.data['x']
376
            d2 = self.data['y']
377
378
        elif ax == 1:
            d = self.data['y']
379
380
381
382
383
384
            d2 = self.data['x']
        
        if orthoRange is not None:
            mask = (d2 >= orthoRange[0]) * (d2 <= orthoRange[1])
            d = d[mask]
            d2 = d2[mask]
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
            
        if frac >= 1.0:
            minIndex = np.argmin(d)
            maxIndex = np.argmax(d)
            minVal = d[minIndex]
            maxVal = d[maxIndex]
            if not self.opts['pxMode']:
                minVal -= self.data[minIndex]['size']
                maxVal += self.data[maxIndex]['size']
            self.bounds[ax] = (minVal, maxVal)
            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)))
            
            

    
    
    
Luke Campagnola's avatar
Luke Campagnola committed
406
407
408
409
410
411
412
413
414
    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)
415
        self.sigPlotChanged.emit(self)
416

Luke Campagnola's avatar
Luke Campagnola committed
417
418
    def defaultSpotPixmap(self):
        ## Return the default spot pixmap
419
        if self._spotPixmap is None:
Luke Campagnola's avatar
Luke Campagnola committed
420
            self._spotPixmap = makeSymbolPixmap(size=self.opts['size'], brush=self.opts['brush'], pen=self.opts['pen'], symbol=self.opts['symbol'])
421
422
423
        return self._spotPixmap

    def boundingRect(self):
424
425
426
427
428
429
430
431
        (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
432
        return QtCore.QRectF(xmn, ymn, xmx-xmn, ymx-ymn)
433
        
434
    def points(self):
Luke Campagnola's avatar
Luke Campagnola committed
435
436
        return self.data['item']
        
437
438
439
440
441
442
    def pointsAt(self, pos):
        x = pos.x()
        y = pos.y()
        pw = self.pixelWidth()
        ph = self.pixelHeight()
        pts = []
Luke Campagnola's avatar
Luke Campagnola committed
443
        for s in self.points():
444
            sp = s.pos()
Luke Campagnola's avatar
Luke Campagnola committed
445
            ss = s.size()
446
447
448
            sx = sp.x()
            sy = sp.y()
            s2x = s2y = ss * 0.5
449
            if self.opts['pxMode']:
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
                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()


Luke Campagnola's avatar
Luke Campagnola committed
476
477
478
479
480
481
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.
    """
482

Luke Campagnola's avatar
Luke Campagnola committed
483
484
485
486
487
488
489
490
491
    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()
492
    
Luke Campagnola's avatar
Luke Campagnola committed
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
    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']
519
        try:
Luke Campagnola's avatar
Luke Campagnola committed
520
521
522
            n = int(symbol)
            symbol = Symbols.keys()[n % len(Symbols)]
        except:
523
            pass
Luke Campagnola's avatar
Luke Campagnola committed
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
        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'])
583
        
Luke Campagnola's avatar
Luke Campagnola committed
584
585
586
        ## If all symbol options are default, use default pixmap
        if symbolOpts == (None, None, -1, ''):
            pixmap = self._plot.defaultSpotPixmap()
587
        else:
Luke Campagnola's avatar
Luke Campagnola committed
588
589
            pixmap = makeSymbolPixmap(size=self.size(), pen=self.pen(), brush=self.brush(), symbol=self.symbol())
        self.setPixmap(pixmap)
590
591


Luke Campagnola's avatar
Luke Campagnola committed
592
593
594
595
class PathSpotItem(SpotItem, QtGui.QGraphicsPathItem):
    def __init__(self, data, plot):
        QtGui.QGraphicsPathItem.__init__(self)
        SpotItem.__init__(self, data, plot)
596

Luke Campagnola's avatar
Luke Campagnola committed
597
598
599
600
601
602
603
    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)