AxisItem.py 36.1 KB
Newer Older
1
from pyqtgraph.Qt import QtGui, QtCore
Luke Campagnola's avatar
Luke Campagnola committed
2
from pyqtgraph.python2_3 import asUnicode
3
4
5
6
7
import numpy as np
from pyqtgraph.Point import Point
import pyqtgraph.debug as debug
import weakref
import pyqtgraph.functions as fn
8
import pyqtgraph as pg
9
from .GraphicsWidget import GraphicsWidget
10
11
12

__all__ = ['AxisItem']
class AxisItem(GraphicsWidget):
Luke Campagnola's avatar
Luke Campagnola committed
13
14
15
16
17
18
19
    """
    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 draw a grid.
    If maxTickLength is negative, ticks point into the plot. 
    """
    
20
21
    def __init__(self, orientation, pen=None, linkView=None, parent=None, maxTickLength=-5, showValues=True):
        """
Luke Campagnola's avatar
Luke Campagnola committed
22
23
24
25
26
27
28
29
30
31
        ==============  ===============================================================
        **Arguments:**
        orientation     one of 'left', 'right', 'top', or 'bottom'
        maxTickLength   (px) maximum length of ticks to draw. Negative values draw
                        into the plot, positive values draw outward.
        linkView        (ViewBox) causes the range of values displayed in the axis
                        to be linked to the visible range of a ViewBox.
        showValues      (bool) Whether to display values adjacent to ticks 
        pen             (QPen) Pen used when drawing ticks.
        ==============  ===============================================================
32
33
34
35
36
        """
        
        GraphicsWidget.__init__(self, parent)
        self.label = QtGui.QGraphicsTextItem(self)
        self.showValues = showValues
Luke Campagnola's avatar
Luke Campagnola committed
37
        self.picture = None
38
39
40
41
42
        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.label.rotate(-90)
Luke Campagnola's avatar
Luke Campagnola committed
43
44
            
        self.style = {
Luke Campagnola's avatar
Luke Campagnola committed
45
            'tickTextOffset': (5, 2),  ## (horizontal, vertical) spacing between text and axis 
Luke Campagnola's avatar
Luke Campagnola committed
46
47
48
49
50
            'tickTextWidth': 30,  ## space reserved for tick text
            'tickTextHeight': 18, 
            'autoExpandTextSpace': True,  ## automatically expand text space if needed
            'tickFont': None,
            'stopAxisAtTick': (False, False),  ## whether axis is drawn to edge of box or to last tick 
Luke Campagnola's avatar
Luke Campagnola committed
51
52
53
54
55
56
            'textFillLimits': [  ## how much of the axis to fill up with tick text, maximally. 
                (0, 0.8),    ## never fill more than 80% of the axis
                (2, 0.6),    ## If we already have 2 ticks with text, fill no more than 60% of the axis
                (4, 0.4),    ## If we already have 4 ticks with text, fill no more than 40% of the axis
                (6, 0.2),    ## If we already have 6 ticks with text, fill no more than 20% of the axis
                ]
Luke Campagnola's avatar
Luke Campagnola committed
57
58
59
60
        }
        
        self.textWidth = 30  ## Keeps track of maximum width / height of tick text 
        self.textHeight = 18
61
62
63
64
        
        self.labelText = ''
        self.labelUnits = ''
        self.labelUnitPrefix=''
Luke Campagnola's avatar
Luke Campagnola committed
65
        self.labelStyle = {}
66
        self.logMode = False
67
        self.tickFont = None
68
69
        
        self.tickLength = maxTickLength
70
        self._tickLevels = None  ## used to override the automatic ticking system with explicit ticks
71
        self.scale = 1.0
72
73
        self.autoSIPrefix = True
        self.autoSIPrefixScale = 1.0
74
        
75
76
77
78
        self.setRange(0, 1)
        
        self.setPen(pen)
        
Luke Campagnola's avatar
Luke Campagnola committed
79
        self._linkedView = None
80
81
        if linkView is not None:
            self.linkToView(linkView)
82
        
83
84
85
86
        self.showLabel(False)
        
        self.grid = False
        #self.setCacheMode(self.DeviceCoordinateCache)
87
        
88
89
90
91
92
93
94
95
    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
96
97
        self.picture = None
        self.prepareGeometryChange()
98
99
        self.update()
        
100
    def setLogMode(self, log):
Luke Campagnola's avatar
Luke Campagnola committed
101
102
103
104
105
        """
        If *log* is True, then ticks are displayed on a logarithmic scale and values
        are adjusted accordingly. (This is usually accessed by changing the log mode 
        of a :func:`PlotItem <pyqtgraph.PlotItem.setLogMode>`)
        """
106
107
108
        self.logMode = log
        self.picture = None
        self.update()
109
        
110
111
112
113
114
115
116
117
    def setTickFont(self, font):
        self.tickFont = font
        self.picture = None
        self.prepareGeometryChange()
        ## Need to re-allocate space depending on font size?
        
        self.update()
        
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
    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
143
        self.picture = None
144
145
        
    def showLabel(self, show=True):
Luke Campagnola's avatar
Luke Campagnola committed
146
        """Show/hide the label text for this axis."""
147
148
149
150
151
152
        #self.drawLabel = show
        self.label.setVisible(show)
        if self.orientation in ['left', 'right']:
            self.setWidth()
        else:
            self.setHeight()
153
154
        if self.autoSIPrefix:
            self.updateAutoSIPrefix()
155
156
        
    def setLabel(self, text=None, units=None, unitPrefix=None, **args):
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
        """Set the text displayed adjacent to the axis.
        
        ============= =============================================================
        Arguments
        text          The text (excluding units) to display on the label for this
                      axis.
        units         The units for this axis. Units should generally be given
                      without any scaling prefix (eg, 'V' instead of 'mV'). The
                      scaling prefix will be automatically prepended based on the
                      range of data displayed.
        **args        All extra keyword arguments become CSS style options for 
                      the <span> tag which will surround the axis label and units.
        ============= =============================================================
        
        The final text generated for the label will look like::
        
            <span style="...options...">{text} (prefix{units})</span>
            
        Each extra keyword argument will become a CSS option in the above template. 
        For example, you can set the font size and color of the label::
        
            labelStyle = {'color': '#FFF', 'font-size': '14pt'}
            axis.setLabel('label text', units='V', **labelStyle)
        
        """
182
183
184
185
186
187
188
189
190
191
192
        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())
Luke Campagnola's avatar
Luke Campagnola committed
193
        self._adjustSize()
Luke Campagnola's avatar
Luke Campagnola committed
194
        self.picture = None
195
196
197
198
        self.update()
            
    def labelString(self):
        if self.labelUnits == '':
199
            if not self.autoSIPrefix or self.autoSIPrefixScale == 1.0:
200
201
                units = ''
            else:
202
                units = asUnicode('(x%g)') % (1.0/self.autoSIPrefixScale)
203
204
        else:
            #print repr(self.labelUnitPrefix), repr(self.labelUnits)
205
            units = asUnicode('(%s%s)') % (self.labelUnitPrefix, self.labelUnits)
206
            
207
        s = asUnicode('%s %s') % (self.labelText, units)
208
        
Luke Campagnola's avatar
Luke Campagnola committed
209
        style = ';'.join(['%s: %s' % (k, self.labelStyle[k]) for k in self.labelStyle])
210
        
211
        return asUnicode("<span style='%s'>%s</span>") % (style, s)
Luke Campagnola's avatar
Luke Campagnola committed
212
213
214
215
216
217
218
    
    def _updateMaxTextSize(self, x):
        ## Informs that the maximum tick size orthogonal to the axis has
        ## changed; we use this to decide whether the item needs to be resized
        ## to accomodate.
        if self.orientation in ['left', 'right']:
            mx = max(self.textWidth, x)
Luke Campagnola's avatar
Luke Campagnola committed
219
            if mx > self.textWidth or mx < self.textWidth-10:
Luke Campagnola's avatar
Luke Campagnola committed
220
221
222
223
224
225
                self.textWidth = mx
                if self.style['autoExpandTextSpace'] is True:
                    self.setWidth()
                    #return True  ## size has changed
        else:
            mx = max(self.textHeight, x)
Luke Campagnola's avatar
Luke Campagnola committed
226
            if mx > self.textHeight or mx < self.textHeight-10:
Luke Campagnola's avatar
Luke Campagnola committed
227
228
229
230
231
232
233
234
235
236
237
                self.textHeight = mx
                if self.style['autoExpandTextSpace'] is True:
                    self.setHeight()
                    #return True  ## size has changed
        
    def _adjustSize(self):
        if self.orientation in ['left', 'right']:
            self.setWidth()
        else:
            self.setHeight()
    
238
    def setHeight(self, h=None):
Luke Campagnola's avatar
Luke Campagnola committed
239
240
        """Set the height of this axis reserved for ticks and tick labels.
        The height of the axis label is automatically added."""
241
        if h is None:
Luke Campagnola's avatar
Luke Campagnola committed
242
243
244
245
            if self.style['autoExpandTextSpace'] is True:
                h = self.textHeight
            else:
                h = self.style['tickTextHeight']
Luke Campagnola's avatar
Luke Campagnola committed
246
            h += max(0, self.tickLength) + self.style['tickTextOffset'][1]
247
            if self.label.isVisible():
Luke Campagnola's avatar
Luke Campagnola committed
248
                h += self.label.boundingRect().height() * 0.8
249
250
        self.setMaximumHeight(h)
        self.setMinimumHeight(h)
Luke Campagnola's avatar
Luke Campagnola committed
251
        self.picture = None
252
253
254
        
        
    def setWidth(self, w=None):
Luke Campagnola's avatar
Luke Campagnola committed
255
256
        """Set the width of this axis reserved for ticks and tick labels.
        The width of the axis label is automatically added."""
257
        if w is None:
Luke Campagnola's avatar
Luke Campagnola committed
258
259
260
261
            if self.style['autoExpandTextSpace'] is True:
                w = self.textWidth
            else:
                w = self.style['tickTextWidth']
Luke Campagnola's avatar
Luke Campagnola committed
262
            w += max(0, self.tickLength) + self.style['tickTextOffset'][0]
263
            if self.label.isVisible():
Luke Campagnola's avatar
Luke Campagnola committed
264
                w += self.label.boundingRect().height() * 0.8  ## bounding rect is usually an overestimate
265
266
        self.setMaximumWidth(w)
        self.setMinimumWidth(w)
Luke Campagnola's avatar
Luke Campagnola committed
267
        self.picture = None
268
        
269
270
271
    def pen(self):
        if self._pen is None:
            return fn.mkPen(pg.getConfigOption('foreground'))
Luke Campagnola's avatar
Luke Campagnola committed
272
        return pg.mkPen(self._pen)
273
        
274
    def setPen(self, pen):
275
276
277
278
279
280
        """
        Set the pen used for drawing text, axes, ticks, and grid lines.
        if pen == None, the default will be used (see :func:`setConfigOption 
        <pyqtgraph.setConfigOption>`)
        """
        self._pen = pen
Luke Campagnola's avatar
Luke Campagnola committed
281
        self.picture = None
Luke Campagnola's avatar
Luke Campagnola committed
282
283
284
285
        if pen is None:
            pen = pg.getConfigOption('foreground')
        self.labelStyle['color'] = '#' + pg.colorStr(pg.mkPen(pen).color())[:6]
        self.setLabel()
286
287
288
289
        self.update()
        
    def setScale(self, scale=None):
        """
290
        Set the value scaling for this axis. 
Luke Campagnola's avatar
Luke Campagnola committed
291
        
292
293
294
        Setting this value causes the axis to draw ticks and tick labels as if
        the view coordinate system were scaled. By default, the axis scaling is 
        1.0.
295
        """
296
297
298
299
300
        # Deprecated usage, kept for backward compatibility
        if scale is None:  
            scale = 1.0
            self.enableAutoSIPrefix(True)
            
301
302
303
        if scale != self.scale:
            self.scale = scale
            self.setLabel()
Luke Campagnola's avatar
Luke Campagnola committed
304
            self.picture = None
305
306
            self.update()
        
307
    def enableAutoSIPrefix(self, enable=True):
308
309
310
311
312
313
314
315
316
317
318
319
320
321
        """
        Enable (or disable) automatic SI prefix scaling on this axis. 
        
        When enabled, this feature automatically determines the best SI prefix 
        to prepend to the label units, while ensuring that axis values are scaled
        accordingly. 
        
        For example, if the axis spans values from -0.1 to 0.1 and has units set 
        to 'V' then the axis would display values -100 to 100
        and the units would appear as 'mV'
        
        This feature is enabled by default, and is only available when a suffix
        (unit string) is provided to display on the label.
        """
322
        self.autoSIPrefix = enable
Luke Campagnola's avatar
Luke Campagnola committed
323
        self.updateAutoSIPrefix()
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
        
    def updateAutoSIPrefix(self):
        if self.label.isVisible():
            (scale, prefix) = fn.siScale(max(abs(self.range[0]*self.scale), abs(self.range[1]*self.scale)))
            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
        
        self.autoSIPrefixScale = scale
        self.picture = None
        self.update()
        
        
340
    def setRange(self, mn, mx):
341
342
        """Set the range of values displayed by the axis.
        Usually this is handled automatically by linking the axis to a ViewBox with :func:`linkToView <pyqtgraph.AxisItem.linkToView>`"""
Luke Campagnola's avatar
Luke Campagnola committed
343
        if any(np.isinf((mn, mx))) or any(np.isnan((mn, mx))):
344
345
            raise Exception("Not setting range to [%s, %s]" % (str(mn), str(mx)))
        self.range = [mn, mx]
346
347
        if self.autoSIPrefix:
            self.updateAutoSIPrefix()
Luke Campagnola's avatar
Luke Campagnola committed
348
        self.picture = None
349
350
        self.update()
        
Luke Campagnola's avatar
Luke Campagnola committed
351
352
353
354
355
356
357
    def linkedView(self):
        """Return the ViewBox this axis is linked to"""
        if self._linkedView is None:
            return None
        else:
            return self._linkedView()
        
358
    def linkToView(self, view):
Luke Campagnola's avatar
Luke Campagnola committed
359
        """Link this axis to a ViewBox, causing its displayed range to match the visible range of the view."""
Luke Campagnola's avatar
Luke Campagnola committed
360
361
        oldView = self.linkedView()
        self._linkedView = weakref.ref(view)
362
        if self.orientation in ['right', 'left']:
Luke Campagnola's avatar
Luke Campagnola committed
363
364
            if oldView is not None:
                oldView.sigYRangeChanged.disconnect(self.linkedViewChanged)
365
366
            view.sigYRangeChanged.connect(self.linkedViewChanged)
        else:
Luke Campagnola's avatar
Luke Campagnola committed
367
368
            if oldView is not None:
                oldView.sigXRangeChanged.disconnect(self.linkedViewChanged)
369
370
            view.sigXRangeChanged.connect(self.linkedViewChanged)
        
Luke Campagnola's avatar
Luke Campagnola committed
371
372
373
374
375
        if oldView is not None:
            oldView.sigResized.disconnect(self.linkedViewChanged)
        view.sigResized.connect(self.linkedViewChanged)
        
    def linkedViewChanged(self, view, newRange=None):
376
        if self.orientation in ['right', 'left']:
Luke Campagnola's avatar
Luke Campagnola committed
377
378
            if newRange is None:
                newRange = view.viewRange()[1]
379
380
381
382
            if view.yInverted():
                self.setRange(*newRange[::-1])
            else:
                self.setRange(*newRange)
Luke Campagnola's avatar
Luke Campagnola committed
383
        else:
Luke Campagnola's avatar
Luke Campagnola committed
384
385
            if newRange is None:
                newRange = view.viewRange()[0]
Luke Campagnola's avatar
Luke Campagnola committed
386
            self.setRange(*newRange)
387
388
        
    def boundingRect(self):
Luke Campagnola's avatar
Luke Campagnola committed
389
390
        linkedView = self.linkedView()
        if linkedView is None or self.grid is False:
391
392
            rect = self.mapRectFromParent(self.geometry())
            ## extend rect if ticks go in negative direction
393
            ## also extend to account for text that flows past the edges
394
            if self.orientation == 'left':
395
                rect = rect.adjusted(0, -15, -min(0,self.tickLength), 15)
396
            elif self.orientation == 'right':
397
                rect = rect.adjusted(min(0,self.tickLength), -15, 0, 15)
398
            elif self.orientation == 'top':
399
                rect = rect.adjusted(-15, 0, 15, -min(0,self.tickLength))
400
            elif self.orientation == 'bottom':
401
                rect = rect.adjusted(-15, min(0,self.tickLength), 15, 0)
402
403
            return rect
        else:
Luke Campagnola's avatar
Luke Campagnola committed
404
            return self.mapRectFromParent(self.geometry()) | linkedView.mapRectToItem(self, linkedView.boundingRect())
405
406
        
    def paint(self, p, opt, widget):
Luke Campagnola's avatar
Luke Campagnola committed
407
408
        if self.picture is None:
            try:
Luke Campagnola's avatar
Luke Campagnola committed
409
410
411
                picture = QtGui.QPicture()
                painter = QtGui.QPainter(picture)
                specs = self.generateDrawSpecs(painter)
412
413
                if specs is not None:
                    self.drawPicture(painter, *specs)
Luke Campagnola's avatar
Luke Campagnola committed
414
415
            finally:
                painter.end()
Luke Campagnola's avatar
Luke Campagnola committed
416
            self.picture = picture
Luke Campagnola's avatar
Luke Campagnola committed
417
418
        #p.setRenderHint(p.Antialiasing, False)   ## Sometimes we get a segfault here ???
        #p.setRenderHint(p.TextAntialiasing, True)
Luke Campagnola's avatar
Luke Campagnola committed
419
420
        self.picture.play(p)
        
Luke Campagnola's avatar
Luke Campagnola committed
421

422
423
424
425
    def setTicks(self, ticks):
        """Explicitly determine which ticks to display.
        This overrides the behavior specified by tickSpacing(), tickValues(), and tickStrings()
        The format for *ticks* looks like::
Luke Campagnola's avatar
Luke Campagnola committed
426

427
428
429
430
431
432
433
434
435
436
437
438
            [
                [ (majorTickValue1, majorTickString1), (majorTickValue2, majorTickString2), ... ],
                [ (minorTickValue1, minorTickString1), (minorTickValue2, minorTickString2), ... ],
                ...
            ]
        
        If *ticks* is None, then the default tick system will be used instead.
        """
        self._tickLevels = ticks
        self.picture = None
        self.update()
    
Luke Campagnola's avatar
Luke Campagnola committed
439
440
441
442
443
444
    def tickSpacing(self, minVal, maxVal, size):
        """Return values describing the desired spacing and offset of ticks.
        
        This method is called whenever the axis needs to be redrawn and is a 
        good method to override in subclasses that require control over tick locations.
        
445
        The return value must be a list of tuples, one for each set of ticks::
Luke Campagnola's avatar
Luke Campagnola committed
446
        
Luke Campagnola's avatar
Luke Campagnola committed
447
448
449
450
451
452
453
454
455
456
457
458
            [
                (major tick spacing, offset),
                (minor tick spacing, offset),
                (sub-minor tick spacing, offset),
                ...
            ]
        """
        dif = abs(maxVal - minVal)
        if dif == 0:
            return []
        
        ## decide optimal minor tick spacing in pixels (this is just aesthetics)
Luke Campagnola's avatar
Luke Campagnola committed
459
        pixelSpacing = size / np.log(size)
460
        optimalTickCount = max(2., size / pixelSpacing)
Luke Campagnola's avatar
Luke Campagnola committed
461
462
463
464
465
466
467
468
469
470
471
472
473
        
        ## optimal minor tick spacing 
        optimalSpacing = dif / optimalTickCount
        
        ## the largest power-of-10 spacing which is smaller than optimal
        p10unit = 10 ** np.floor(np.log10(optimalSpacing))
        
        ## Determine major/minor tick spacings which flank the optimal spacing.
        intervals = np.array([1., 2., 10., 20., 100.]) * p10unit
        minorIndex = 0
        while intervals[minorIndex+1] <= optimalSpacing:
            minorIndex += 1
            
474
        levels = [
Luke Campagnola's avatar
Luke Campagnola committed
475
476
            (intervals[minorIndex+2], 0),
            (intervals[minorIndex+1], 0),
Luke Campagnola's avatar
Luke Campagnola committed
477
            #(intervals[minorIndex], 0)    ## Pretty, but eats up CPU
Luke Campagnola's avatar
Luke Campagnola committed
478
479
        ]
        
480
481
482
483
484
485
486
487
488
        ## decide whether to include the last level of ticks
        minSpacing = min(size / 20., 30.)
        maxTickCount = size / minSpacing
        if dif / intervals[minorIndex] <= maxTickCount:
            levels.append((intervals[minorIndex], 0))
        return levels
        
        
        
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
        ##### This does not work -- switching between 2/5 confuses the automatic text-level-selection
        ### Determine major/minor tick spacings which flank the optimal spacing.
        #intervals = np.array([1., 2., 5., 10., 20., 50., 100.]) * p10unit
        #minorIndex = 0
        #while intervals[minorIndex+1] <= optimalSpacing:
            #minorIndex += 1
            
        ### make sure we never see 5 and 2 at the same time
        #intIndexes = [
            #[0,1,3],
            #[0,2,3],
            #[2,3,4],
            #[3,4,6],
            #[3,5,6],
        #][minorIndex]
        
        #return [
            #(intervals[intIndexes[2]], 0),
            #(intervals[intIndexes[1]], 0),
            #(intervals[intIndexes[0]], 0)
        #]
        
        
Luke Campagnola's avatar
Luke Campagnola committed
512
513
514

    def tickValues(self, minVal, maxVal, size):
        """
Luke Campagnola's avatar
Luke Campagnola committed
515
516
517
518
519
520
521
        Return the values and spacing of ticks to draw::
        
            [  
                (spacing, [major ticks]), 
                (spacing, [minor ticks]), 
                ... 
            ]
Luke Campagnola's avatar
Luke Campagnola committed
522
523
524
525
        
        By default, this method calls tickSpacing to determine the correct tick locations.
        This is a good method to override in subclasses.
        """
Luke Campagnola's avatar
Luke Campagnola committed
526
527
        minVal, maxVal = sorted((minVal, maxVal))
        
Luke Campagnola's avatar
Luke Campagnola committed
528
529
530
531

        minVal *= self.scale  
        maxVal *= self.scale
        #size *= self.scale
532
            
Luke Campagnola's avatar
Luke Campagnola committed
533
534
        ticks = []
        tickLevels = self.tickSpacing(minVal, maxVal, size)
Luke Campagnola's avatar
Luke Campagnola committed
535
        allValues = np.array([])
Luke Campagnola's avatar
Luke Campagnola committed
536
537
538
539
540
541
542
543
        for i in range(len(tickLevels)):
            spacing, offset = tickLevels[i]
            
            ## determine starting tick
            start = (np.ceil((minVal-offset) / spacing) * spacing) + offset
            
            ## determine number of ticks
            num = int((maxVal-start) / spacing) + 1
Luke Campagnola's avatar
Luke Campagnola committed
544
            values = (np.arange(num) * spacing + start) / self.scale
Luke Campagnola's avatar
Luke Campagnola committed
545
546
547
            ## remove any ticks that were present in higher levels
            ## we assume here that if the difference between a tick value and a previously seen tick value
            ## is less than spacing/100, then they are 'equal' and we can ignore the new tick.
548
            values = list(filter(lambda x: all(np.abs(allValues-x) > spacing*0.01), values) )
Luke Campagnola's avatar
Luke Campagnola committed
549
            allValues = np.concatenate([allValues, values])
Luke Campagnola's avatar
Luke Campagnola committed
550
            ticks.append((spacing/self.scale, values))
551
552
553
            
        if self.logMode:
            return self.logTickValues(minVal, maxVal, size, ticks)
Luke Campagnola's avatar
Luke Campagnola committed
554
555
556
557
558
559
560
561
562
        
        
        #nticks = []
        #for t in ticks:
            #nvals = []
            #for v in t[1]:
                #nvals.append(v/self.scale)
            #nticks.append((t[0]/self.scale,nvals))
        #ticks = nticks
563
            
Luke Campagnola's avatar
Luke Campagnola committed
564
565
        return ticks
    
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
    def logTickValues(self, minVal, maxVal, size, stdTicks):
        
        ## start with the tick spacing given by tickValues().
        ## Any level whose spacing is < 1 needs to be converted to log scale
        
        ticks = []
        for (spacing, t) in stdTicks:
            if spacing >= 1.0:
                ticks.append((spacing, t))
        
        if len(ticks) < 3:
            v1 = int(np.floor(minVal))
            v2 = int(np.ceil(maxVal))
            #major = list(range(v1+1, v2))
            
            minor = []
            for v in range(v1, v2):
                minor.extend(v + np.log10(np.arange(1, 10)))
            minor = [x for x in minor if x>minVal and x<maxVal]
            ticks.append((None, minor))
        return ticks
Luke Campagnola's avatar
Luke Campagnola committed
587
588
589
590
591
592
593
594
595
596
597
598
599
600

    def tickStrings(self, values, scale, spacing):
        """Return the strings that should be placed next to ticks. This method is called 
        when redrawing the axis and is a good method to override in subclasses.
        The method is called with a list of tick values, a scaling factor (see below), and the 
        spacing between ticks (this is required since, in some instances, there may be only 
        one tick and thus no other way to determine the tick spacing)
        
        The scale argument is used when the axis label is displaying units which may have an SI scaling prefix.
        When determining the text to display, use value*scale to correctly account for this prefix.
        For example, if the axis label's units are set to 'V', then a tick value of 0.001 might
        be accompanied by a scale value of 1000. This indicates that the label is displaying 'mV', and 
        thus the tick should display 0.001 * 1000 = 1.
        """
601
602
603
        if self.logMode:
            return self.logTickStrings(values, scale, spacing)
        
Luke Campagnola's avatar
Luke Campagnola committed
604
605
606
607
608
609
610
611
612
613
        places = max(0, np.ceil(-np.log10(spacing*scale)))
        strings = []
        for v in values:
            vs = v * scale
            if abs(vs) < .001 or abs(vs) >= 10000:
                vstr = "%g" % vs
            else:
                vstr = ("%%0.%df" % places) % vs
            strings.append(vstr)
        return strings
Luke Campagnola's avatar
Luke Campagnola committed
614
        
615
616
617
    def logTickStrings(self, values, scale, spacing):
        return ["%0.1g"%x for x in 10 ** np.array(values).astype(float)]
        
Luke Campagnola's avatar
Luke Campagnola committed
618
619
620
621
622
623
624
    def generateDrawSpecs(self, p):
        """
        Calls tickValues() and tickStrings to determine where and how ticks should
        be drawn, then generates from this a set of drawing commands to be 
        interpreted by drawPicture().
        """
        prof = debug.Profiler("AxisItem.generateDrawSpecs", disabled=True)
625
626
627
628
        
        #bounds = self.boundingRect()
        bounds = self.mapRectFromParent(self.geometry())
        
Luke Campagnola's avatar
Luke Campagnola committed
629
630
        linkedView = self.linkedView()
        if linkedView is None or self.grid is False:
Luke Campagnola's avatar
Luke Campagnola committed
631
            tickBounds = bounds
632
        else:
Luke Campagnola's avatar
Luke Campagnola committed
633
            tickBounds = linkedView.mapRectToItem(self, linkedView.boundingRect())
634
635
636
        
        if self.orientation == 'left':
            span = (bounds.topRight(), bounds.bottomRight())
Luke Campagnola's avatar
Luke Campagnola committed
637
            tickStart = tickBounds.right()
638
639
640
641
642
            tickStop = bounds.right()
            tickDir = -1
            axis = 0
        elif self.orientation == 'right':
            span = (bounds.topLeft(), bounds.bottomLeft())
Luke Campagnola's avatar
Luke Campagnola committed
643
            tickStart = tickBounds.left()
644
645
646
647
648
            tickStop = bounds.left()
            tickDir = 1
            axis = 0
        elif self.orientation == 'top':
            span = (bounds.bottomLeft(), bounds.bottomRight())
Luke Campagnola's avatar
Luke Campagnola committed
649
            tickStart = tickBounds.bottom()
650
651
652
653
654
            tickStop = bounds.bottom()
            tickDir = -1
            axis = 1
        elif self.orientation == 'bottom':
            span = (bounds.topLeft(), bounds.topRight())
Luke Campagnola's avatar
Luke Campagnola committed
655
            tickStart = tickBounds.top()
656
657
658
            tickStop = bounds.top()
            tickDir = 1
            axis = 1
Luke Campagnola's avatar
Luke Campagnola committed
659
660
        #print tickStart, tickStop, span
        
661
        ## determine size of this item in pixels
662
        points = list(map(self.mapToDevice, span))
Luke Campagnola's avatar
Luke Campagnola committed
663
664
        if None in points:
            return
665
        lengthInPixels = Point(points[1] - points[0]).length()
Luke Campagnola's avatar
Luke Campagnola committed
666
667
        if lengthInPixels == 0:
            return
668

669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
        if self._tickLevels is None:
            tickLevels = self.tickValues(self.range[0], self.range[1], lengthInPixels)
            tickStrings = None
        else:
            ## parse self.tickLevels into the formats returned by tickLevels() and tickStrings()
            tickLevels = []
            tickStrings = []
            for level in self._tickLevels:
                values = []
                strings = []
                tickLevels.append((None, values))
                tickStrings.append(strings)
                for val, strn in level:
                    values.append(val)
                    strings.append(strn)
684
        
Luke Campagnola's avatar
Luke Campagnola committed
685
        textLevel = 1  ## draw text at this scale level
686
        
Luke Campagnola's avatar
Luke Campagnola committed
687
688
        ## determine mapping between tick values and local coordinates
        dif = self.range[1] - self.range[0]
689
690
691
        if dif == 0:
            xscale = 1
            offset = 0
692
        else:
693
694
695
696
697
698
            if axis == 0:
                xScale = -bounds.height() / dif
                offset = self.range[0] * xScale - bounds.height()
            else:
                xScale = bounds.width() / dif
                offset = self.range[0] * xScale
699
700
701
702
            
        xRange = [x * xScale - offset for x in self.range]
        xMin = min(xRange)
        xMax = max(xRange)
703
704
705
        
        prof.mark('init')
            
Luke Campagnola's avatar
Luke Campagnola committed
706
707
708
        tickPositions = [] # remembers positions of previously drawn ticks
        
        ## draw ticks
709
710
        ## (to improve performance, we do not interleave line and text drawing, since this causes unnecessary pipeline switching)
        ## draw three different intervals, long ticks first
Luke Campagnola's avatar
Luke Campagnola committed
711
        tickSpecs = []
Luke Campagnola's avatar
Luke Campagnola committed
712
713
714
        for i in range(len(tickLevels)):
            tickPositions.append([])
            ticks = tickLevels[i][1]
715
716
        
            ## length of tick
717
            tickLength = self.tickLength / ((i*0.5)+1.0)
Luke Campagnola's avatar
Luke Campagnola committed
718
                
Luke Campagnola's avatar
Luke Campagnola committed
719
            lineAlpha = 255 / (i+1)
Luke Campagnola's avatar
Luke Campagnola committed
720
            if self.grid is not False:
Luke Campagnola's avatar
Luke Campagnola committed
721
                lineAlpha *= self.grid/255. * np.clip((0.05  * lengthInPixels / (len(ticks)+1)), 0., 1.)
722
            
Luke Campagnola's avatar
Luke Campagnola committed
723
            for v in ticks:
724
                ## determine actual position to draw this tick
Luke Campagnola's avatar
Luke Campagnola committed
725
                x = (v * xScale) - offset
726
727
728
729
730
                if x < xMin or x > xMax:  ## last check to make sure no out-of-bounds ticks are drawn
                    tickPositions[i].append(None)
                    continue
                tickPositions[i].append(x)
                
Luke Campagnola's avatar
Luke Campagnola committed
731
732
                p1 = [x, x]
                p2 = [x, x]
733
                p1[axis] = tickStart
Luke Campagnola's avatar
Luke Campagnola committed
734
735
                p2[axis] = tickStop
                if self.grid is False:
Luke Campagnola's avatar
Luke Campagnola committed
736
                    p2[axis] += tickLength*tickDir
737
738
739
740
                tickPen = self.pen()
                color = tickPen.color()
                color.setAlpha(lineAlpha)
                tickPen.setColor(color)
Luke Campagnola's avatar
Luke Campagnola committed
741
742
                tickSpecs.append((tickPen, Point(p1), Point(p2)))
        prof.mark('compute ticks')
743

Luke Campagnola's avatar
Luke Campagnola committed
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
        ## This is where the long axis line should be drawn
        
        if self.style['stopAxisAtTick'][0] is True:
            stop = max(span[0].y(), min(map(min, tickPositions)))
            if axis == 0:
                span[0].setY(stop)
            else:
                span[0].setX(stop)
        if self.style['stopAxisAtTick'][1] is True:
            stop = min(span[1].y(), max(map(max, tickPositions)))
            if axis == 0:
                span[1].setY(stop)
            else:
                span[1].setX(stop)
        axisSpec = (self.pen(), span[0], span[1])

        
        
Luke Campagnola's avatar
Luke Campagnola committed
762
        textOffset = self.style['tickTextOffset'][axis]  ## spacing between axis and text
Luke Campagnola's avatar
Luke Campagnola committed
763
764
765
766
767
768
769
        #if self.style['autoExpandTextSpace'] is True:
            #textWidth = self.textWidth
            #textHeight = self.textHeight
        #else:
            #textWidth = self.style['tickTextWidth'] ## space allocated for horizontal text
            #textHeight = self.style['tickTextHeight'] ## space allocated for horizontal text
            
Luke Campagnola's avatar
Luke Campagnola committed
770
        textSize2 = 0
771
        textRects = []
Luke Campagnola's avatar
Luke Campagnola committed
772
        textSpecs = []  ## list of draw
Luke Campagnola's avatar
Luke Campagnola committed
773
        textSize2 = 0
Luke Campagnola's avatar
Luke Campagnola committed
774
        for i in range(len(tickLevels)):
775
776
777
            ## Get the list of strings to display for this level
            if tickStrings is None:
                spacing, values = tickLevels[i]
778
                strings = self.tickStrings(values, self.autoSIPrefixScale * self.scale, spacing)
Luke Campagnola's avatar
Luke Campagnola committed
779
            else:
780
                strings = tickStrings[i]
Luke Campagnola's avatar
Luke Campagnola committed
781
                
782
            if len(strings) == 0:
Luke Campagnola's avatar
Luke Campagnola committed
783
                continue
784
785
786
787
788
            
            ## ignore strings belonging to ticks that were previously ignored
            for j in range(len(strings)):
                if tickPositions[i][j] is None:
                    strings[j] = None
789

Luke Campagnola's avatar
Luke Campagnola committed
790
791
792
793
794
795
796
797
798
799
800
801
802
803
            ## Measure density of text; decide whether to draw this level
            rects = []
            for s in strings:
                if s is None:
                    rects.append(None)
                else:
                    br = p.boundingRect(QtCore.QRectF(0, 0, 100, 100), QtCore.Qt.AlignCenter, str(s))
                    ## boundingRect is usually just a bit too large
                    ## (but this probably depends on per-font metrics?)
                    br.setHeight(br.height() * 0.8)
                    
                    rects.append(br)
                    textRects.append(rects[-1])
            
804
805
806
807
            if i > 0:  ## always draw top level
                ## measure all text, make sure there's enough room
                if axis == 0:
                    textSize = np.sum([r.height() for r in textRects])
Luke Campagnola's avatar
Luke Campagnola committed
808
                    textSize2 = np.max([r.width() for r in textRects])
809
810
                else:
                    textSize = np.sum([r.width() for r in textRects])
Luke Campagnola's avatar
Luke Campagnola committed
811
                    textSize2 = np.max([r.height() for r in textRects])
812

Luke Campagnola's avatar
Luke Campagnola committed
813
814
815
                ## If the strings are too crowded, stop drawing text now.
                ## We use three different crowding limits based on the number
                ## of texts drawn so far.
816
                textFillRatio = float(textSize) / lengthInPixels
Luke Campagnola's avatar
Luke Campagnola committed
817
818
819
820
821
822
                finished = False
                for nTexts, limit in self.style['textFillLimits']:
                    if len(textSpecs) >= nTexts and textFillRatio >= limit:
                        finished = True
                        break
                if finished:
823
                    break
Luke Campagnola's avatar
Luke Campagnola committed
824
            
825
826
827
828
            #spacing, values = tickLevels[best]
            #strings = self.tickStrings(values, self.scale, spacing)
            for j in range(len(strings)):
                vstr = strings[j]
829
                if vstr is None: ## this tick was ignored because it is out of bounds
830
                    continue
831
                vstr = str(vstr)
832
                x = tickPositions[i][j]
Luke Campagnola's avatar
Luke Campagnola committed
833
834
                #textRect = p.boundingRect(QtCore.QRectF(0, 0, 100, 100), QtCore.Qt.AlignCenter, vstr)
                textRect = rects[j]
835
                height = textRect.height()
Luke Campagnola's avatar
Luke Campagnola committed
836
837
838
                width = textRect.width()
                #self.textHeight = height
                offset = max(0,self.tickLength) + textOffset
839
                if self.orientation == 'left':
Luke Campagnola's avatar
Luke Campagnola committed
840
841
                    textFlags = QtCore.Qt.TextDontClip|QtCore.Qt.AlignRight|QtCore.Qt.AlignVCenter
                    rect = QtCore.QRectF(tickStop-offset-width, x-(height/2), width, height)
842
                elif self.orientation == 'right':
Luke Campagnola's avatar
Luke Campagnola committed
843
844
                    textFlags = QtCore.Qt.TextDontClip|QtCore.Qt.AlignLeft|QtCore.Qt.AlignVCenter
                    rect = QtCore.QRectF(tickStop+offset, x-(height/2), width, height)
845
                elif self.orientation == 'top':
Luke Campagnola's avatar
Luke Campagnola committed
846
847
                    textFlags = QtCore.Qt.TextDontClip|QtCore.Qt.AlignCenter|QtCore.Qt.AlignBottom
                    rect = QtCore.QRectF(x-width/2., tickStop-offset-height, width, height)
848
                elif self.orientation == 'bottom':
Luke Campagnola's avatar
Luke Campagnola committed
849
850
                    textFlags = QtCore.Qt.TextDontClip|QtCore.Qt.AlignCenter|QtCore.Qt.AlignTop
                    rect = QtCore.QRectF(x-width/2., tickStop+offset, width, height)
851

Luke Campagnola's avatar
Luke Campagnola committed
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
                #p.setPen(self.pen())
                #p.drawText(rect, textFlags, vstr)
                textSpecs.append((rect, textFlags, vstr))
        prof.mark('compute text')
        
        ## update max text size if needed.
        self._updateMaxTextSize(textSize2)
        
        return (axisSpec, tickSpecs, textSpecs)
    
    def drawPicture(self, p, axisSpec, tickSpecs, textSpecs):
        prof = debug.Profiler("AxisItem.drawPicture", disabled=True)
        
        p.setRenderHint(p.Antialiasing, False)
        p.setRenderHint(p.TextAntialiasing, True)
        
        ## draw long line along axis
        pen, p1, p2 = axisSpec
        p.setPen(pen)
        p.drawLine(p1, p2)
        p.translate(0.5,0)  ## resolves some damn pixel ambiguity
        
        ## draw ticks
        for pen, p1, p2 in tickSpecs:
            p.setPen(pen)
            p.drawLine(p1, p2)
        prof.mark('draw ticks')
        
        ## Draw all text
        if self.tickFont is not None:
            p.setFont(self.tickFont)
        p.setPen(self.pen())
        for rect, flags, text in textSpecs:
            p.drawText(rect, flags, text)
            #p.drawRect(rect)
        
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
        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
907
908
        if self.linkedView() is None: 
            return
909
910
911
912
913
        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
914
915
916
917
918
919
920
921
922
923
924
925
926
        
    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)