AxisItem.py 18.5 KB
Newer Older
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
from pyqtgraph.Qt import QtGui, QtCore
import numpy as np
from pyqtgraph.Point import Point
import pyqtgraph.debug as debug
import weakref
import pyqtgraph.functions as fn
from GraphicsWidget import GraphicsWidget

__all__ = ['AxisItem']
class AxisItem(GraphicsWidget):
    def __init__(self, orientation, pen=None, linkView=None, parent=None, maxTickLength=-5, showValues=True):
        """
        GraphicsItem showing a single plot axis with ticks, values, and label.
        Can be configured to fit on any side of a plot, and can automatically synchronize its displayed scale with ViewBox items.
        Ticks can be extended to make a grid.
        """
        
        
        GraphicsWidget.__init__(self, parent)
        self.label = QtGui.QGraphicsTextItem(self)
        self.showValues = showValues
Luke Campagnola's avatar
Luke Campagnola committed
22
        self.picture = None
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
        self.orientation = orientation
        if orientation not in ['left', 'right', 'top', 'bottom']:
            raise Exception("Orientation argument must be one of 'left', 'right', 'top', or 'bottom'.")
        if orientation in ['left', 'right']:
            #self.setMinimumWidth(25)
            #self.setSizePolicy(QtGui.QSizePolicy(
                #QtGui.QSizePolicy.Minimum,
                #QtGui.QSizePolicy.Expanding
            #))
            self.label.rotate(-90)
        #else:
            #self.setMinimumHeight(50)
            #self.setSizePolicy(QtGui.QSizePolicy(
                #QtGui.QSizePolicy.Expanding,
                #QtGui.QSizePolicy.Minimum
            #))
        #self.drawLabel = False
        
        self.labelText = ''
        self.labelUnits = ''
        self.labelUnitPrefix=''
        self.labelStyle = {'color': '#CCC'}
        
        self.textHeight = 18
        self.tickLength = maxTickLength
        self.scale = 1.0
        self.autoScale = True
            
        self.setRange(0, 1)
        
        if pen is None:
            pen = QtGui.QPen(QtGui.QColor(100, 100, 100))
        self.setPen(pen)
        
Luke Campagnola's avatar
Luke Campagnola committed
57
        self._linkedView = None
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
        if linkView is not None:
            self.linkToView(linkView)
            
        self.showLabel(False)
        
        self.grid = False
        #self.setCacheMode(self.DeviceCoordinateCache)
            
    def close(self):
        self.scene().removeItem(self.label)
        self.label = None
        self.scene().removeItem(self)
        
    def setGrid(self, grid):
        """Set the alpha value for the grid, or False to disable."""
        self.grid = grid
Luke Campagnola's avatar
Luke Campagnola committed
74
75
        self.picture = None
        self.prepareGeometryChange()
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
        self.update()
        
        
    def resizeEvent(self, ev=None):
        #s = self.size()
        
        ## Set the position of the label
        nudge = 5
        br = self.label.boundingRect()
        p = QtCore.QPointF(0, 0)
        if self.orientation == 'left':
            p.setY(int(self.size().height()/2 + br.width()/2))
            p.setX(-nudge)
            #s.setWidth(10)
        elif self.orientation == 'right':
            #s.setWidth(10)
            p.setY(int(self.size().height()/2 + br.width()/2))
            p.setX(int(self.size().width()-br.height()+nudge))
        elif self.orientation == 'top':
            #s.setHeight(10)
            p.setY(-nudge)
            p.setX(int(self.size().width()/2. - br.width()/2.))
        elif self.orientation == 'bottom':
            p.setX(int(self.size().width()/2. - br.width()/2.))
            #s.setHeight(10)
            p.setY(int(self.size().height()-br.height()+nudge))
        #self.label.resize(s)
        self.label.setPos(p)
Luke Campagnola's avatar
Luke Campagnola committed
104
        self.picture = None
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
        
    def showLabel(self, show=True):
        #self.drawLabel = show
        self.label.setVisible(show)
        if self.orientation in ['left', 'right']:
            self.setWidth()
        else:
            self.setHeight()
        if self.autoScale:
            self.setScale()
        
    def setLabel(self, text=None, units=None, unitPrefix=None, **args):
        if text is not None:
            self.labelText = text
            self.showLabel()
        if units is not None:
            self.labelUnits = units
            self.showLabel()
        if unitPrefix is not None:
            self.labelUnitPrefix = unitPrefix
        if len(args) > 0:
            self.labelStyle = args
        self.label.setHtml(self.labelString())
        self.resizeEvent()
Luke Campagnola's avatar
Luke Campagnola committed
129
        self.picture = None
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
        self.update()
            
    def labelString(self):
        if self.labelUnits == '':
            if self.scale == 1.0:
                units = ''
            else:
                units = u'(x%g)' % (1.0/self.scale)
        else:
            #print repr(self.labelUnitPrefix), repr(self.labelUnits)
            units = u'(%s%s)' % (self.labelUnitPrefix, self.labelUnits)
            
        s = u'%s %s' % (self.labelText, units)
        
        style = ';'.join(['%s: "%s"' % (k, self.labelStyle[k]) for k in self.labelStyle])
        
        return u"<span style='%s'>%s</span>" % (style, s)
        
    def setHeight(self, h=None):
        if h is None:
            h = self.textHeight + max(0, self.tickLength)
            if self.label.isVisible():
                h += self.textHeight
        self.setMaximumHeight(h)
        self.setMinimumHeight(h)
Luke Campagnola's avatar
Luke Campagnola committed
155
        self.picture = None
156
157
158
159
160
161
162
163
164
165
166
167
        
        
    def setWidth(self, w=None):
        if w is None:
            w = max(0, self.tickLength) + 40
            if self.label.isVisible():
                w += self.textHeight
        self.setMaximumWidth(w)
        self.setMinimumWidth(w)
        
    def setPen(self, pen):
        self.pen = pen
Luke Campagnola's avatar
Luke Campagnola committed
168
        self.picture = None
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
        self.update()
        
    def setScale(self, scale=None):
        """
        Set the value scaling for this axis. 
        The scaling value 1) multiplies the values displayed along the axis
        and 2) changes the way units are displayed in the label. 
        For example:
            If the axis spans values from -0.1 to 0.1 and has units set to 'V'
            then a scale of 1000 would cause the axis to display values -100 to 100
            and the units would appear as 'mV'
        If scale is None, then it will be determined automatically based on the current 
        range displayed by the axis.
        """
        if scale is None:
            #if self.drawLabel:  ## If there is a label, then we are free to rescale the values 
            if self.label.isVisible():
                d = self.range[1] - self.range[0]
Luke Campagnola's avatar
Luke Campagnola committed
187
188
                #(scale, prefix) = fn.siScale(d / 2.)
                (scale, prefix) = fn.siScale(max(abs(self.range[0]), abs(self.range[1])))
189
190
191
192
193
194
195
196
197
198
199
                if self.labelUnits == '' and prefix in ['k', 'm']:  ## If we are not showing units, wait until 1e6 before scaling.
                    scale = 1.0
                    prefix = ''
                self.setLabel(unitPrefix=prefix)
            else:
                scale = 1.0
        
        
        if scale != self.scale:
            self.scale = scale
            self.setLabel()
Luke Campagnola's avatar
Luke Campagnola committed
200
            self.picture = None
201
202
203
204
205
206
207
208
            self.update()
        
    def setRange(self, mn, mx):
        if mn in [np.nan, np.inf, -np.inf] or mx in [np.nan, np.inf, -np.inf]:
            raise Exception("Not setting range to [%s, %s]" % (str(mn), str(mx)))
        self.range = [mn, mx]
        if self.autoScale:
            self.setScale()
Luke Campagnola's avatar
Luke Campagnola committed
209
        self.picture = None
210
211
        self.update()
        
Luke Campagnola's avatar
Luke Campagnola committed
212
213
214
215
216
217
218
    def linkedView(self):
        """Return the ViewBox this axis is linked to"""
        if self._linkedView is None:
            return None
        else:
            return self._linkedView()
        
219
    def linkToView(self, view):
Luke Campagnola's avatar
Luke Campagnola committed
220
221
        oldView = self.linkedView()
        self._linkedView = weakref.ref(view)
222
        if self.orientation in ['right', 'left']:
Luke Campagnola's avatar
Luke Campagnola committed
223
224
            if oldView is not None:
                oldView.sigYRangeChanged.disconnect(self.linkedViewChanged)
225
226
            view.sigYRangeChanged.connect(self.linkedViewChanged)
        else:
Luke Campagnola's avatar
Luke Campagnola committed
227
228
            if oldView is not None:
                oldView.sigXRangeChanged.disconnect(self.linkedViewChanged)
229
230
231
232
233
234
            view.sigXRangeChanged.connect(self.linkedViewChanged)
        
    def linkedViewChanged(self, view, newRange):
        self.setRange(*newRange)
        
    def boundingRect(self):
Luke Campagnola's avatar
Luke Campagnola committed
235
236
        linkedView = self.linkedView()
        if linkedView is None or self.grid is False:
237
238
239
240
241
242
243
244
245
246
247
248
            rect = self.mapRectFromParent(self.geometry())
            ## extend rect if ticks go in negative direction
            if self.orientation == 'left':
                rect.setRight(rect.right() - min(0,self.tickLength))
            elif self.orientation == 'right':
                rect.setLeft(rect.left() + min(0,self.tickLength))
            elif self.orientation == 'top':
                rect.setBottom(rect.bottom() - min(0,self.tickLength))
            elif self.orientation == 'bottom':
                rect.setTop(rect.top() + min(0,self.tickLength))
            return rect
        else:
Luke Campagnola's avatar
Luke Campagnola committed
249
            return self.mapRectFromParent(self.geometry()) | linkedView.mapRectToItem(self, linkedView.boundingRect())
250
251
        
    def paint(self, p, opt, widget):
Luke Campagnola's avatar
Luke Campagnola committed
252
253
254
255
256
257
258
259
260
261
262
263
        if self.picture is None:
            self.picture = QtGui.QPicture()
            painter = QtGui.QPainter(self.picture)
            try:
                self.drawPicture(painter)
            finally:
                painter.end()
        self.picture.play(p)
        
        
    def drawPicture(self, p):
        
264
265
266
267
268
269
        prof = debug.Profiler("AxisItem.paint", disabled=True)
        p.setPen(self.pen)
        
        #bounds = self.boundingRect()
        bounds = self.mapRectFromParent(self.geometry())
        
Luke Campagnola's avatar
Luke Campagnola committed
270
271
        linkedView = self.linkedView()
        if linkedView is None or self.grid is False:
272
273
            tbounds = bounds
        else:
Luke Campagnola's avatar
Luke Campagnola committed
274
            tbounds = linkedView.mapRectToItem(self, linkedView.boundingRect())
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
        
        if self.orientation == 'left':
            span = (bounds.topRight(), bounds.bottomRight())
            tickStart = tbounds.right()
            tickStop = bounds.right()
            tickDir = -1
            axis = 0
        elif self.orientation == 'right':
            span = (bounds.topLeft(), bounds.bottomLeft())
            tickStart = tbounds.left()
            tickStop = bounds.left()
            tickDir = 1
            axis = 0
        elif self.orientation == 'top':
            span = (bounds.bottomLeft(), bounds.bottomRight())
            tickStart = tbounds.bottom()
            tickStop = bounds.bottom()
            tickDir = -1
            axis = 1
        elif self.orientation == 'bottom':
            span = (bounds.topLeft(), bounds.topRight())
            tickStart = tbounds.top()
            tickStop = bounds.top()
            tickDir = 1
            axis = 1

        ## draw long line along axis
        p.drawLine(*span)

        ## determine size of this item in pixels
        points = map(self.mapToDevice, span)
        lengthInPixels = Point(points[1] - points[0]).length()

        ## decide optimal tick spacing in pixels
Luke Campagnola's avatar
Luke Campagnola committed
309
        pixelSpacing = np.log(lengthInPixels+10) * 2
310
311
312
313
314
315
316
317
318
319
320
321
        optimalTickCount = lengthInPixels / pixelSpacing

        ## Determine optimal tick spacing
        #intervals = [1., 2., 5., 10., 20., 50.]
        #intervals = [1., 2.5, 5., 10., 25., 50.]
        intervals = np.array([0.1, 0.2, 1., 2., 10., 20., 100., 200.])
        dif = abs(self.range[1] - self.range[0])
        if dif == 0.0:
            return
        pw = 10 ** (np.floor(np.log10(dif))-1)
        scaledIntervals = intervals * pw
        scaledTickCounts = dif / scaledIntervals 
Luke Campagnola's avatar
Luke Campagnola committed
322
323
324
325
326
327
328
329
330
331
332
333
        try:
            i1 = np.argwhere(scaledTickCounts < optimalTickCount)[0,0]
        except:
            print "AxisItem can't determine tick spacing:"
            print "scaledTickCounts", scaledTickCounts
            print "optimalTickCount", optimalTickCount
            print "dif", dif
            print "scaledIntervals", scaledIntervals
            print "intervals", intervals
            print "pw", pw
            print "pixelSpacing", pixelSpacing
            i1 = 1
334
335
336
337
338
339
340
341
342
343
        
        distBetweenIntervals = (optimalTickCount-scaledTickCounts[i1]) / (scaledTickCounts[i1-1]-scaledTickCounts[i1])
        
        #print optimalTickCount, i1, scaledIntervals, distBetweenIntervals
        
        #for i in range(len(intervals)):
            #i1 = i
            #if dif / (pw*intervals[i]) < 10:
                #break
        
Luke Campagnola's avatar
Luke Campagnola committed
344
        textLevel = 1  ## draw text at this scale level
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
        
        #print "range: %s   dif: %f   power: %f  interval: %f   spacing: %f" % (str(self.range), dif, pw, intervals[i1], sp)
        
        #print "  start at %f,  %d ticks" % (start, num)
        
        
        if axis == 0:
            xs = -bounds.height() / dif
        else:
            xs = bounds.width() / dif
        
        prof.mark('init')
            
        tickPositions = set() # remembers positions of previously drawn ticks
        ## draw ticks and generate list of texts to draw
        ## (to improve performance, we do not interleave line and text drawing, since this causes unnecessary pipeline switching)
        ## draw three different intervals, long ticks first
        texts = []
        for i in [2,1,0]:
Luke Campagnola's avatar
Luke Campagnola committed
364
365
            if i1+i >= len(intervals) or i1+i < 0:
                print "AxisItem.paint error: i1=%d, i=%d, len(intervals)=%d" % (i1, i, len(intervals))
366
                continue
Luke Campagnola's avatar
Luke Campagnola committed
367
            
Luke Campagnola's avatar
Luke Campagnola committed
368
            ## spacing for this interval
369
370
371
372
373
374
375
376
377
378
379
380
381
            sp = pw*intervals[i1+i]
            
            ## determine starting tick
            start = np.ceil(self.range[0] / sp) * sp
            
            ## determine number of ticks
            num = int(dif / sp) + 1
            
            ## last tick value
            last = start + sp * num
            
            ## Number of decimal places to print
            maxVal = max(abs(start), abs(last))
Luke Campagnola's avatar
Luke Campagnola committed
382
383
            places = max(0, np.ceil(-np.log10(sp*self.scale)))
            #print i, sp, sp*self.scale, np.log10(sp*self.scale), places
384
385
386
387
388
389
390
391
392
393
394
395
396
397
        
            ## length of tick
            #h = np.clip((self.tickLength*3 / num) - 1., min(0, self.tickLength), max(0, self.tickLength))
            if i == 0:
                h = self.tickLength * distBetweenIntervals / 2.
            else:
                h = self.tickLength*i/2.
                
            ## alpha
            if i == 0:
                #a = min(255, (765. / num) - 1.)
                a = 255 * distBetweenIntervals
            else:
                a = 255
Luke Campagnola's avatar
Luke Campagnola committed
398
399
400
401
402
403
                
            lineAlpha = a
            textAlpha = a
                
            if self.grid is not False:
                lineAlpha = int(lineAlpha * self.grid / 255.)
404
405
406
407
408
409
410
411
412
413
414
415
            
            if axis == 0:
                offset = self.range[0] * xs - bounds.height()
            else:
                offset = self.range[0] * xs
            
            for j in range(num):
                v = start + sp * j
                x = (v * xs) - offset
                p1 = [0, 0]
                p2 = [0, 0]
                p1[axis] = tickStart
Luke Campagnola's avatar
Luke Campagnola committed
416
417
418
                p2[axis] = tickStop
                if self.grid is False:
                    p2[axis] += h*tickDir
419
420
421
422
423
424
                p1[1-axis] = p2[1-axis] = x
                
                if p1[1-axis] > [bounds.width(), bounds.height()][1-axis]:
                    continue
                if p1[1-axis] < 0:
                    continue
Luke Campagnola's avatar
Luke Campagnola committed
425
                p.setPen(QtGui.QPen(QtGui.QColor(150, 150, 150, lineAlpha)))
Luke Campagnola's avatar
Luke Campagnola committed
426
                
427
428
                # draw tick only if there is none
                tickPos = p1[1-axis]
Luke Campagnola's avatar
Luke Campagnola committed
429
430
431
432
433
434
435
436
437
438
                
                #if tickPos not in tickPositions:
                p.drawLine(Point(p1), Point(p2))
                #tickPositions.add(tickPos)
                if i == textLevel:
                    if abs(v*self.scale) < .001 or abs(v*self.scale) >= 10000:
                        vstr = "%g" % (v * self.scale)
                    else:
                        vstr = ("%%0.%df" % places) % (v * self.scale)
                    #print "    ", v*self.scale, places, vstr
439
                        
Luke Campagnola's avatar
Luke Campagnola committed
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
                    textRect = p.boundingRect(QtCore.QRectF(0, 0, 100, 100), QtCore.Qt.AlignCenter, vstr)
                    height = textRect.height()
                    self.textHeight = height
                    if self.orientation == 'left':
                        textFlags = QtCore.Qt.AlignRight|QtCore.Qt.AlignVCenter
                        rect = QtCore.QRectF(tickStop-100, x-(height/2), 99-max(0,self.tickLength), height)
                    elif self.orientation == 'right':
                        textFlags = QtCore.Qt.AlignLeft|QtCore.Qt.AlignVCenter
                        rect = QtCore.QRectF(tickStop+max(0,self.tickLength)+1, x-(height/2), 100-max(0,self.tickLength), height)
                    elif self.orientation == 'top':
                        textFlags = QtCore.Qt.AlignCenter|QtCore.Qt.AlignBottom
                        rect = QtCore.QRectF(x-100, tickStop-max(0,self.tickLength)-height, 200, height)
                    elif self.orientation == 'bottom':
                        textFlags = QtCore.Qt.AlignCenter|QtCore.Qt.AlignTop
                        rect = QtCore.QRectF(x-100, tickStop+max(0,self.tickLength), 200, height)
                    
                    #p.setPen(QtGui.QPen(QtGui.QColor(150, 150, 150, a)))
                    #p.drawText(rect, textFlags, vstr)
                    texts.append((rect, textFlags, vstr, textAlpha))
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
                    
        prof.mark('draw ticks')
        for args in texts:
            p.setPen(QtGui.QPen(QtGui.QColor(150, 150, 150, args[3])))
            p.drawText(*args[:3])
        prof.mark('draw text')
        prof.finish()
        
    def show(self):
        
        if self.orientation in ['left', 'right']:
            self.setWidth()
        else:
            self.setHeight()
        GraphicsWidget.show(self)
        
    def hide(self):
        if self.orientation in ['left', 'right']:
            self.setWidth(0)
        else:
            self.setHeight(0)
        GraphicsWidget.hide(self)

    def wheelEvent(self, ev):
Luke Campagnola's avatar
Luke Campagnola committed
483
484
        if self.linkedView() is None: 
            return
485
486
487
488
489
        if self.orientation in ['left', 'right']:
            self.linkedView().wheelEvent(ev, axis=1)
        else:
            self.linkedView().wheelEvent(ev, axis=0)
        ev.accept()
Luke Campagnola's avatar
Luke Campagnola committed
490
491
492
493
494
495
496
497
498
499
500
501
502
        
    def mouseDragEvent(self, event):
        if self.linkedView() is None: 
            return
        if self.orientation in ['left', 'right']:
            return self.linkedView().mouseDragEvent(event, axis=1)
        else:
            return self.linkedView().mouseDragEvent(event, axis=0)
        
    def mouseClickEvent(self, event):
        if self.linkedView() is None: 
            return
        return self.linkedView().mouseClickEvent(event)