AxisItem.py 34.3 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
45
46
47
48
49
50
51
52
53
54
            
        self.style = {
            'tickTextOffset': 3,  ## spacing between text and axis
            '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 
        }
        
        self.textWidth = 30  ## Keeps track of maximum width / height of tick text 
        self.textHeight = 18
55
56
57
58
        
        self.labelText = ''
        self.labelUnits = ''
        self.labelUnitPrefix=''
Luke Campagnola's avatar
Luke Campagnola committed
59
        self.labelStyle = {}
60
        self.logMode = False
61
        self.tickFont = None
62
63
        
        self.tickLength = maxTickLength
64
        self._tickLevels = None  ## used to override the automatic ticking system with explicit ticks
65
66
        self.scale = 1.0
        self.autoScale = True
67
        
68
69
70
71
        self.setRange(0, 1)
        
        self.setPen(pen)
        
Luke Campagnola's avatar
Luke Campagnola committed
72
        self._linkedView = None
73
74
        if linkView is not None:
            self.linkToView(linkView)
75
        
76
77
78
79
        self.showLabel(False)
        
        self.grid = False
        #self.setCacheMode(self.DeviceCoordinateCache)
80
        
81
82
83
84
85
86
87
88
    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
89
90
        self.picture = None
        self.prepareGeometryChange()
91
92
        self.update()
        
93
    def setLogMode(self, log):
Luke Campagnola's avatar
Luke Campagnola committed
94
95
96
97
98
        """
        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>`)
        """
99
100
101
        self.logMode = log
        self.picture = None
        self.update()
102
        
103
104
105
106
107
108
109
110
    def setTickFont(self, font):
        self.tickFont = font
        self.picture = None
        self.prepareGeometryChange()
        ## Need to re-allocate space depending on font size?
        
        self.update()
        
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
    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
136
        self.picture = None
137
138
        
    def showLabel(self, show=True):
Luke Campagnola's avatar
Luke Campagnola committed
139
        """Show/hide the label text for this axis."""
140
141
142
143
144
145
146
147
148
149
        #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):
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
        """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)
        
        """
175
176
177
178
179
180
181
182
183
184
185
        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
186
        self._adjustSize()
Luke Campagnola's avatar
Luke Campagnola committed
187
        self.picture = None
188
189
190
191
192
193
194
        self.update()
            
    def labelString(self):
        if self.labelUnits == '':
            if self.scale == 1.0:
                units = ''
            else:
195
                units = asUnicode('(x%g)') % (1.0/self.scale)
196
197
        else:
            #print repr(self.labelUnitPrefix), repr(self.labelUnits)
198
            units = asUnicode('(%s%s)') % (self.labelUnitPrefix, self.labelUnits)
199
            
200
        s = asUnicode('%s %s') % (self.labelText, units)
201
        
Luke Campagnola's avatar
Luke Campagnola committed
202
        style = ';'.join(['%s: %s' % (k, self.labelStyle[k]) for k in self.labelStyle])
203
        
204
        return asUnicode("<span style='%s'>%s</span>") % (style, s)
Luke Campagnola's avatar
Luke Campagnola committed
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
    
    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)
            if mx > self.textWidth:
                self.textWidth = mx
                if self.style['autoExpandTextSpace'] is True:
                    self.setWidth()
                    #return True  ## size has changed
        else:
            mx = max(self.textHeight, x)
            if mx > self.textHeight:
                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()
    
231
    def setHeight(self, h=None):
Luke Campagnola's avatar
Luke Campagnola committed
232
233
        """Set the height of this axis reserved for ticks and tick labels.
        The height of the axis label is automatically added."""
234
        if h is None:
Luke Campagnola's avatar
Luke Campagnola committed
235
236
237
238
239
            if self.style['autoExpandTextSpace'] is True:
                h = self.textHeight
            else:
                h = self.style['tickTextHeight']
            h += max(0, self.tickLength) + self.style['tickTextOffset']
240
            if self.label.isVisible():
Luke Campagnola's avatar
Luke Campagnola committed
241
                h += self.label.boundingRect().height() * 0.8
242
243
        self.setMaximumHeight(h)
        self.setMinimumHeight(h)
Luke Campagnola's avatar
Luke Campagnola committed
244
        self.picture = None
245
246
247
        
        
    def setWidth(self, w=None):
Luke Campagnola's avatar
Luke Campagnola committed
248
249
        """Set the width of this axis reserved for ticks and tick labels.
        The width of the axis label is automatically added."""
250
        if w is None:
Luke Campagnola's avatar
Luke Campagnola committed
251
252
253
254
255
            if self.style['autoExpandTextSpace'] is True:
                w = self.textWidth
            else:
                w = self.style['tickTextWidth']
            w += max(0, self.tickLength) + self.style['tickTextOffset']
256
            if self.label.isVisible():
Luke Campagnola's avatar
Luke Campagnola committed
257
                w += self.label.boundingRect().height() * 0.8  ## bounding rect is usually an overestimate
258
259
        self.setMaximumWidth(w)
        self.setMinimumWidth(w)
Luke Campagnola's avatar
Luke Campagnola committed
260
        self.picture = None
261
        
262
263
264
    def pen(self):
        if self._pen is None:
            return fn.mkPen(pg.getConfigOption('foreground'))
Luke Campagnola's avatar
Luke Campagnola committed
265
        return pg.mkPen(self._pen)
266
        
267
    def setPen(self, pen):
268
269
270
271
272
273
        """
        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
274
        self.picture = None
Luke Campagnola's avatar
Luke Campagnola committed
275
276
277
278
        if pen is None:
            pen = pg.getConfigOption('foreground')
        self.labelStyle['color'] = '#' + pg.colorStr(pg.mkPen(pen).color())[:6]
        self.setLabel()
279
280
281
282
        self.update()
        
    def setScale(self, scale=None):
        """
Luke Campagnola's avatar
Luke Campagnola committed
283
284
285
286
        Set the value scaling for this axis. Values on the axis are multiplied
        by this scale factor before being displayed as text. By default,
        this scaling value is automatically determined based on the visible range
        and the axis units are updated to reflect the chosen scale factor.
Luke Campagnola's avatar
Luke Campagnola committed
287
288
289
290
        
        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'
291
292
293
294
        """
        if scale is None:
            #if self.drawLabel:  ## If there is a label, then we are free to rescale the values 
            if self.label.isVisible():
Luke Campagnola's avatar
Luke Campagnola committed
295
                #d = self.range[1] - self.range[0]
Luke Campagnola's avatar
Luke Campagnola committed
296
297
                #(scale, prefix) = fn.siScale(d / 2.)
                (scale, prefix) = fn.siScale(max(abs(self.range[0]), abs(self.range[1])))
298
299
300
301
302
303
                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
Luke Campagnola's avatar
Luke Campagnola committed
304
305
306
307
        else:
            self.setLabel(unitPrefix='')
            self.autoScale = False
            
308
309
310
        if scale != self.scale:
            self.scale = scale
            self.setLabel()
Luke Campagnola's avatar
Luke Campagnola committed
311
            self.picture = None
312
313
314
            self.update()
        
    def setRange(self, mn, mx):
315
316
        """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
317
        if any(np.isinf((mn, mx))) or any(np.isnan((mn, mx))):
318
319
320
321
            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
322
        self.picture = None
323
324
        self.update()
        
Luke Campagnola's avatar
Luke Campagnola committed
325
326
327
328
329
330
331
    def linkedView(self):
        """Return the ViewBox this axis is linked to"""
        if self._linkedView is None:
            return None
        else:
            return self._linkedView()
        
332
    def linkToView(self, view):
Luke Campagnola's avatar
Luke Campagnola committed
333
        """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
334
335
        oldView = self.linkedView()
        self._linkedView = weakref.ref(view)
336
        if self.orientation in ['right', 'left']:
Luke Campagnola's avatar
Luke Campagnola committed
337
338
            if oldView is not None:
                oldView.sigYRangeChanged.disconnect(self.linkedViewChanged)
339
340
            view.sigYRangeChanged.connect(self.linkedViewChanged)
        else:
Luke Campagnola's avatar
Luke Campagnola committed
341
342
            if oldView is not None:
                oldView.sigXRangeChanged.disconnect(self.linkedViewChanged)
343
344
            view.sigXRangeChanged.connect(self.linkedViewChanged)
        
Luke Campagnola's avatar
Luke Campagnola committed
345
346
347
348
349
        if oldView is not None:
            oldView.sigResized.disconnect(self.linkedViewChanged)
        view.sigResized.connect(self.linkedViewChanged)
        
    def linkedViewChanged(self, view, newRange=None):
350
        if self.orientation in ['right', 'left']:
Luke Campagnola's avatar
Luke Campagnola committed
351
352
            if newRange is None:
                newRange = view.viewRange()[1]
353
354
355
356
            if view.yInverted():
                self.setRange(*newRange[::-1])
            else:
                self.setRange(*newRange)
Luke Campagnola's avatar
Luke Campagnola committed
357
        else:
Luke Campagnola's avatar
Luke Campagnola committed
358
359
            if newRange is None:
                newRange = view.viewRange()[0]
Luke Campagnola's avatar
Luke Campagnola committed
360
            self.setRange(*newRange)
361
362
        
    def boundingRect(self):
Luke Campagnola's avatar
Luke Campagnola committed
363
364
        linkedView = self.linkedView()
        if linkedView is None or self.grid is False:
365
366
            rect = self.mapRectFromParent(self.geometry())
            ## extend rect if ticks go in negative direction
367
            ## also extend to account for text that flows past the edges
368
            if self.orientation == 'left':
369
                rect = rect.adjusted(0, -15, -min(0,self.tickLength), 15)
370
            elif self.orientation == 'right':
371
                rect = rect.adjusted(min(0,self.tickLength), -15, 0, 15)
372
            elif self.orientation == 'top':
373
                rect = rect.adjusted(-15, 0, 15, -min(0,self.tickLength))
374
            elif self.orientation == 'bottom':
375
                rect = rect.adjusted(-15, min(0,self.tickLength), 15, 0)
376
377
            return rect
        else:
Luke Campagnola's avatar
Luke Campagnola committed
378
            return self.mapRectFromParent(self.geometry()) | linkedView.mapRectToItem(self, linkedView.boundingRect())
379
380
        
    def paint(self, p, opt, widget):
Luke Campagnola's avatar
Luke Campagnola committed
381
382
        if self.picture is None:
            try:
Luke Campagnola's avatar
Luke Campagnola committed
383
384
385
                picture = QtGui.QPicture()
                painter = QtGui.QPainter(picture)
                specs = self.generateDrawSpecs(painter)
386
387
                if specs is not None:
                    self.drawPicture(painter, *specs)
Luke Campagnola's avatar
Luke Campagnola committed
388
389
            finally:
                painter.end()
Luke Campagnola's avatar
Luke Campagnola committed
390
            self.picture = picture
Luke Campagnola's avatar
Luke Campagnola committed
391
392
        #p.setRenderHint(p.Antialiasing, False)   ## Sometimes we get a segfault here ???
        #p.setRenderHint(p.TextAntialiasing, True)
Luke Campagnola's avatar
Luke Campagnola committed
393
394
        self.picture.play(p)
        
Luke Campagnola's avatar
Luke Campagnola committed
395

396
397
398
399
    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
400

401
402
403
404
405
406
407
408
409
410
411
412
            [
                [ (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
413
414
415
416
417
418
    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.
        
419
        The return value must be a list of tuples, one for each set of ticks::
Luke Campagnola's avatar
Luke Campagnola committed
420
        
Luke Campagnola's avatar
Luke Campagnola committed
421
422
423
424
425
426
427
428
429
430
431
432
433
            [
                (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)
        pixelSpacing = np.log(size+10) * 5
434
        optimalTickCount = max(2., size / pixelSpacing)
Luke Campagnola's avatar
Luke Campagnola committed
435
436
437
438
439
440
441
442
443
444
445
446
447
        
        ## 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
            
448
        levels = [
Luke Campagnola's avatar
Luke Campagnola committed
449
450
            (intervals[minorIndex+2], 0),
            (intervals[minorIndex+1], 0),
Luke Campagnola's avatar
Luke Campagnola committed
451
            #(intervals[minorIndex], 0)    ## Pretty, but eats up CPU
Luke Campagnola's avatar
Luke Campagnola committed
452
453
        ]
        
454
455
456
457
458
459
460
461
462
        ## 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
        
        
        
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
        ##### 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
486
487
488

    def tickValues(self, minVal, maxVal, size):
        """
Luke Campagnola's avatar
Luke Campagnola committed
489
490
491
492
493
494
495
        Return the values and spacing of ticks to draw::
        
            [  
                (spacing, [major ticks]), 
                (spacing, [minor ticks]), 
                ... 
            ]
Luke Campagnola's avatar
Luke Campagnola committed
496
497
498
499
        
        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
500
501
        minVal, maxVal = sorted((minVal, maxVal))
        
502
            
Luke Campagnola's avatar
Luke Campagnola committed
503
504
        ticks = []
        tickLevels = self.tickSpacing(minVal, maxVal, size)
Luke Campagnola's avatar
Luke Campagnola committed
505
        allValues = np.array([])
Luke Campagnola's avatar
Luke Campagnola committed
506
507
508
509
510
511
512
513
        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
514
            values = np.arange(num) * spacing + start
Luke Campagnola's avatar
Luke Campagnola committed
515
516
517
            ## 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.
518
            values = list(filter(lambda x: all(np.abs(allValues-x) > spacing*0.01), values) )
Luke Campagnola's avatar
Luke Campagnola committed
519
            allValues = np.concatenate([allValues, values])
520
            ticks.append((spacing, values))
521
522
523
524
            
        if self.logMode:
            return self.logTickValues(minVal, maxVal, size, ticks)
            
Luke Campagnola's avatar
Luke Campagnola committed
525
526
        return ticks
    
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
    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
548
549
550
551
552
553
554
555
556
557
558
559
560
561

    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.
        """
562
563
564
        if self.logMode:
            return self.logTickStrings(values, scale, spacing)
        
Luke Campagnola's avatar
Luke Campagnola committed
565
566
567
568
569
570
571
572
573
574
        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
575
        
576
577
578
    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
579
580
581
582
583
584
585
    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)
586
587
588
589
        
        #bounds = self.boundingRect()
        bounds = self.mapRectFromParent(self.geometry())
        
Luke Campagnola's avatar
Luke Campagnola committed
590
591
        linkedView = self.linkedView()
        if linkedView is None or self.grid is False:
Luke Campagnola's avatar
Luke Campagnola committed
592
            tickBounds = bounds
593
        else:
Luke Campagnola's avatar
Luke Campagnola committed
594
            tickBounds = linkedView.mapRectToItem(self, linkedView.boundingRect())
595
596
597
        
        if self.orientation == 'left':
            span = (bounds.topRight(), bounds.bottomRight())
Luke Campagnola's avatar
Luke Campagnola committed
598
            tickStart = tickBounds.right()
599
600
601
602
603
            tickStop = bounds.right()
            tickDir = -1
            axis = 0
        elif self.orientation == 'right':
            span = (bounds.topLeft(), bounds.bottomLeft())
Luke Campagnola's avatar
Luke Campagnola committed
604
            tickStart = tickBounds.left()
605
606
607
608
609
            tickStop = bounds.left()
            tickDir = 1
            axis = 0
        elif self.orientation == 'top':
            span = (bounds.bottomLeft(), bounds.bottomRight())
Luke Campagnola's avatar
Luke Campagnola committed
610
            tickStart = tickBounds.bottom()
611
612
613
614
615
            tickStop = bounds.bottom()
            tickDir = -1
            axis = 1
        elif self.orientation == 'bottom':
            span = (bounds.topLeft(), bounds.topRight())
Luke Campagnola's avatar
Luke Campagnola committed
616
            tickStart = tickBounds.top()
617
618
619
            tickStop = bounds.top()
            tickDir = 1
            axis = 1
Luke Campagnola's avatar
Luke Campagnola committed
620
621
        #print tickStart, tickStop, span
        
622
        ## determine size of this item in pixels
623
        points = list(map(self.mapToDevice, span))
Luke Campagnola's avatar
Luke Campagnola committed
624
625
        if None in points:
            return
626
        lengthInPixels = Point(points[1] - points[0]).length()
Luke Campagnola's avatar
Luke Campagnola committed
627
628
        if lengthInPixels == 0:
            return
629

630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
        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)
645
        
Luke Campagnola's avatar
Luke Campagnola committed
646
        textLevel = 1  ## draw text at this scale level
647
        
Luke Campagnola's avatar
Luke Campagnola committed
648
649
        ## determine mapping between tick values and local coordinates
        dif = self.range[1] - self.range[0]
650
651
652
        if dif == 0:
            xscale = 1
            offset = 0
653
        else:
654
655
656
657
658
659
            if axis == 0:
                xScale = -bounds.height() / dif
                offset = self.range[0] * xScale - bounds.height()
            else:
                xScale = bounds.width() / dif
                offset = self.range[0] * xScale
660
661
662
663
            
        xRange = [x * xScale - offset for x in self.range]
        xMin = min(xRange)
        xMax = max(xRange)
664
665
666
        
        prof.mark('init')
            
Luke Campagnola's avatar
Luke Campagnola committed
667
668
669
        tickPositions = [] # remembers positions of previously drawn ticks
        
        ## draw ticks
670
671
        ## (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
672
        tickSpecs = []
Luke Campagnola's avatar
Luke Campagnola committed
673
674
675
        for i in range(len(tickLevels)):
            tickPositions.append([])
            ticks = tickLevels[i][1]
676
677
        
            ## length of tick
678
            tickLength = self.tickLength / ((i*0.5)+1.0)
Luke Campagnola's avatar
Luke Campagnola committed
679
                
Luke Campagnola's avatar
Luke Campagnola committed
680
            lineAlpha = 255 / (i+1)
Luke Campagnola's avatar
Luke Campagnola committed
681
            if self.grid is not False:
Luke Campagnola's avatar
Luke Campagnola committed
682
                lineAlpha *= self.grid/255. * np.clip((0.05  * lengthInPixels / (len(ticks)+1)), 0., 1.)
683
            
Luke Campagnola's avatar
Luke Campagnola committed
684
            for v in ticks:
685
                ## determine actual position to draw this tick
Luke Campagnola's avatar
Luke Campagnola committed
686
                x = (v * xScale) - offset
687
688
689
690
691
                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
692
693
                p1 = [x, x]
                p2 = [x, x]
694
                p1[axis] = tickStart
Luke Campagnola's avatar
Luke Campagnola committed
695
696
                p2[axis] = tickStop
                if self.grid is False:
Luke Campagnola's avatar
Luke Campagnola committed
697
                    p2[axis] += tickLength*tickDir
698
699
700
701
                tickPen = self.pen()
                color = tickPen.color()
                color.setAlpha(lineAlpha)
                tickPen.setColor(color)
Luke Campagnola's avatar
Luke Campagnola committed
702
703
                tickSpecs.append((tickPen, Point(p1), Point(p2)))
        prof.mark('compute ticks')
704

Luke Campagnola's avatar
Luke Campagnola committed
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
        ## 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])

        
        
        textOffset = self.style['tickTextOffset']  ## spacing between axis and text
        #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
            
731
        
732
        textRects = []
Luke Campagnola's avatar
Luke Campagnola committed
733
        textSpecs = []  ## list of draw
Luke Campagnola's avatar
Luke Campagnola committed
734
        textSize2 = 0
Luke Campagnola's avatar
Luke Campagnola committed
735
        for i in range(len(tickLevels)):
736
737
738
739
            ## Get the list of strings to display for this level
            if tickStrings is None:
                spacing, values = tickLevels[i]
                strings = self.tickStrings(values, self.scale, spacing)
Luke Campagnola's avatar
Luke Campagnola committed
740
            else:
741
                strings = tickStrings[i]
Luke Campagnola's avatar
Luke Campagnola committed
742
                
743
            if len(strings) == 0:
Luke Campagnola's avatar
Luke Campagnola committed
744
                continue
745
746
747
748
749
            
            ## ignore strings belonging to ticks that were previously ignored
            for j in range(len(strings)):
                if tickPositions[i][j] is None:
                    strings[j] = None
750

Luke Campagnola's avatar
Luke Campagnola committed
751
752
753
754
755
756
757
758
759
760
761
762
763
764
            ## 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])
            
765
766
767
768
            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
769
                    textSize2 = np.max([r.width() for r in textRects])
770
771
                else:
                    textSize = np.sum([r.width() for r in textRects])
Luke Campagnola's avatar
Luke Campagnola committed
772
                    textSize2 = np.max([r.height() for r in textRects])
773
774
775
776
777

                ## If the strings are too crowded, stop drawing text now
                textFillRatio = float(textSize) / lengthInPixels
                if textFillRatio > 0.7:
                    break
Luke Campagnola's avatar
Luke Campagnola committed
778
            
779
780
781
782
            #spacing, values = tickLevels[best]
            #strings = self.tickStrings(values, self.scale, spacing)
            for j in range(len(strings)):
                vstr = strings[j]
783
                if vstr is None: ## this tick was ignored because it is out of bounds
784
                    continue
785
                vstr = str(vstr)
786
                x = tickPositions[i][j]
Luke Campagnola's avatar
Luke Campagnola committed
787
788
                #textRect = p.boundingRect(QtCore.QRectF(0, 0, 100, 100), QtCore.Qt.AlignCenter, vstr)
                textRect = rects[j]
789
                height = textRect.height()
Luke Campagnola's avatar
Luke Campagnola committed
790
791
792
                width = textRect.width()
                #self.textHeight = height
                offset = max(0,self.tickLength) + textOffset
793
                if self.orientation == 'left':
Luke Campagnola's avatar
Luke Campagnola committed
794
795
                    textFlags = QtCore.Qt.TextDontClip|QtCore.Qt.AlignRight|QtCore.Qt.AlignVCenter
                    rect = QtCore.QRectF(tickStop-offset-width, x-(height/2), width, height)
796
                elif self.orientation == 'right':
Luke Campagnola's avatar
Luke Campagnola committed
797
798
                    textFlags = QtCore.Qt.TextDontClip|QtCore.Qt.AlignLeft|QtCore.Qt.AlignVCenter
                    rect = QtCore.QRectF(tickStop+offset, x-(height/2), width, height)
799
                elif self.orientation == 'top':
Luke Campagnola's avatar
Luke Campagnola committed
800
801
                    textFlags = QtCore.Qt.TextDontClip|QtCore.Qt.AlignCenter|QtCore.Qt.AlignBottom
                    rect = QtCore.QRectF(x-width/2., tickStop-offset-height, width, height)
802
                elif self.orientation == 'bottom':
Luke Campagnola's avatar
Luke Campagnola committed
803
804
                    textFlags = QtCore.Qt.TextDontClip|QtCore.Qt.AlignCenter|QtCore.Qt.AlignTop
                    rect = QtCore.QRectF(x-width/2., tickStop+offset, width, height)
805

Luke Campagnola's avatar
Luke Campagnola committed
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
                #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)
        
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
        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
861
862
        if self.linkedView() is None: 
            return
863
864
865
866
867
        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
868
869
870
871
872
873
874
875
876
877
878
879
880
        
    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)