AxisItem.py 41.7 KB
Newer Older
1
2
from ..Qt import QtGui, QtCore
from ..python2_3 import asUnicode
3
import numpy as np
4
5
from ..Point import Point
from .. import debug as debug
6
import weakref
7
8
from .. import functions as fn
from .. import getConfigOption
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
        """
        
        GraphicsWidget.__init__(self, parent)
        self.label = QtGui.QGraphicsTextItem(self)
Luke Campagnola's avatar
Luke Campagnola committed
36
        self.picture = None
37
38
39
40
41
        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
42
43
            
        self.style = {
Luke Campagnola's avatar
Luke Campagnola committed
44
            'tickTextOffset': [5, 2],  ## (horizontal, vertical) spacing between text and axis 
Luke Campagnola's avatar
Luke Campagnola committed
45
46
47
48
49
            '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
50
51
52
53
54
            '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
55
56
                ],
            'showValues': showValues,
Luke Campagnola's avatar
Luke Campagnola committed
57
            'tickLength': maxTickLength,
58
59
            'maxTickLevel': 2,
            'maxTextLevel': 2,
Luke Campagnola's avatar
Luke Campagnola committed
60
61
62
63
        }
        
        self.textWidth = 30  ## Keeps track of maximum width / height of tick text 
        self.textHeight = 18
64
65
66
67
        
        self.labelText = ''
        self.labelUnits = ''
        self.labelUnitPrefix=''
Luke Campagnola's avatar
Luke Campagnola committed
68
        self.labelStyle = {}
69
        self.logMode = False
70
        self.tickFont = None
71
        
72
        self._tickLevels = None  ## used to override the automatic ticking system with explicit ticks
73
        self._tickSpacing = None  # used to override default tickSpacing method
74
        self.scale = 1.0
75
76
        self.autoSIPrefix = True
        self.autoSIPrefixScale = 1.0
77
        
78
79
        self.setRange(0, 1)
        
80
81
82
83
        if pen is None:
            self.setPen()
        else:
            self.setPen(pen)
84
        
Luke Campagnola's avatar
Luke Campagnola committed
85
        self._linkedView = None
86
87
        if linkView is not None:
            self.linkToView(linkView)
88
        
89
90
91
92
        self.showLabel(False)
        
        self.grid = False
        #self.setCacheMode(self.DeviceCoordinateCache)
Luke Campagnola's avatar
Luke Campagnola committed
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128

    def setStyle(self, **kwds):
        """
        Set various style options.
        
        =================== =======================================================
        Keyword Arguments:
        tickLength          (int) The maximum length of ticks in pixels. 
                            Positive values point toward the text; negative 
                            values point away.
        tickTextOffset      (int) reserved spacing between text and axis in px
        tickTextWidth       (int) Horizontal space reserved for tick text in px
        tickTextHeight      (int) Vertical space reserved for tick text in px
        autoExpandTextSpace (bool) Automatically expand text space if the tick
                            strings become too long.
        tickFont            (QFont or None) Determines the font used for tick 
                            values. Use None for the default font.
        stopAxisAtTick      (tuple: (bool min, bool max)) If True, the axis 
                            line is drawn only as far as the last tick. 
                            Otherwise, the line is drawn to the edge of the 
                            AxisItem boundary.
        textFillLimits      (list of (tick #, % fill) tuples). This structure
                            determines how the AxisItem decides how many ticks 
                            should have text appear next to them. Each tuple in
                            the list specifies what fraction of the axis length
                            may be occupied by text, given the number of ticks
                            that already have text displayed. For example::
                            
                                [(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
129
        showValues          (bool) indicates whether text is displayed adjacent
Luke Campagnola's avatar
Luke Campagnola committed
130
131
                            to ticks.
        =================== =======================================================
Luke Campagnola's avatar
Luke Campagnola committed
132
133
        
        Added in version 0.9.9
Luke Campagnola's avatar
Luke Campagnola committed
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
        """
        for kwd,value in kwds.items():
            if kwd not in self.style:
                raise NameError("%s is not a valid style argument." % kwd)
            
            if kwd in ('tickLength', 'tickTextOffset', 'tickTextWidth', 'tickTextHeight'):
                if not isinstance(value, int):
                    raise ValueError("Argument '%s' must be int" % kwd)
            
            if kwd == 'tickTextOffset':
                if self.orientation in ('left', 'right'):
                    self.style['tickTextOffset'][0] = value
                else:
                    self.style['tickTextOffset'][1] = value
            elif kwd == 'stopAxisAtTick':
                try:
                    assert len(value) == 2 and isinstance(value[0], bool) and isinstance(value[1], bool)
                except:
                    raise ValueError("Argument 'stopAxisAtTick' must have type (bool, bool)")
                self.style[kwd] = value
            else:
                self.style[kwd] = value
        
        self.picture = None
        self._adjustSize()
        self.update()
160
        
161
162
163
164
165
166
    def close(self):
        self.scene().removeItem(self.label)
        self.label = None
        self.scene().removeItem(self)
        
    def setGrid(self, grid):
167
168
169
170
171
        """Set the alpha value (0-255) for the grid, or False to disable.
        
        When grid lines are enabled, the axis tick lines are extended to cover
        the extent of the linked ViewBox, if any.
        """
172
        self.grid = grid
Luke Campagnola's avatar
Luke Campagnola committed
173
174
        self.picture = None
        self.prepareGeometryChange()
175
176
        self.update()
        
177
    def setLogMode(self, log):
Luke Campagnola's avatar
Luke Campagnola committed
178
179
180
181
182
        """
        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>`)
        """
183
184
185
        self.logMode = log
        self.picture = None
        self.update()
186
        
187
188
189
190
191
192
193
194
    def setTickFont(self, font):
        self.tickFont = font
        self.picture = None
        self.prepareGeometryChange()
        ## Need to re-allocate space depending on font size?
        
        self.update()
        
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
    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)
        elif self.orientation == 'right':
            p.setY(int(self.size().height()/2 + br.width()/2))
            p.setX(int(self.size().width()-br.height()+nudge))
        elif self.orientation == 'top':
            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.))
            p.setY(int(self.size().height()-br.height()+nudge))
        self.label.setPos(p)
Luke Campagnola's avatar
Luke Campagnola committed
215
        self.picture = None
216
217
        
    def showLabel(self, show=True):
Luke Campagnola's avatar
Luke Campagnola committed
218
        """Show/hide the label text for this axis."""
219
220
221
222
223
224
        #self.drawLabel = show
        self.label.setVisible(show)
        if self.orientation in ['left', 'right']:
            self.setWidth()
        else:
            self.setHeight()
225
226
        if self.autoSIPrefix:
            self.updateAutoSIPrefix()
227
228
        
    def setLabel(self, text=None, units=None, unitPrefix=None, **args):
229
230
        """Set the text displayed adjacent to the axis.
        
tommy3001's avatar
tommy3001 committed
231
232
233
234
235
236
237
238
        ==============  =============================================================
        **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.
Luke Campagnola's avatar
Luke Campagnola committed
239
        **args          All extra keyword arguments become CSS style options for
tommy3001's avatar
tommy3001 committed
240
241
                        the <span> tag which will surround the axis label and units.
        ==============  =============================================================
242
243
244
245
246
247
248
249
250
251
252
253
        
        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)
        
        """
254
255
256
257
258
259
260
261
262
263
264
        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
265
        self._adjustSize()
Luke Campagnola's avatar
Luke Campagnola committed
266
        self.picture = None
267
268
269
270
        self.update()
            
    def labelString(self):
        if self.labelUnits == '':
271
            if not self.autoSIPrefix or self.autoSIPrefixScale == 1.0:
272
273
                units = ''
            else:
274
                units = asUnicode('(x%g)') % (1.0/self.autoSIPrefixScale)
275
276
        else:
            #print repr(self.labelUnitPrefix), repr(self.labelUnits)
277
            units = asUnicode('(%s%s)') % (asUnicode(self.labelUnitPrefix), asUnicode(self.labelUnits))
278
            
279
        s = asUnicode('%s %s') % (asUnicode(self.labelText), asUnicode(units))
280
        
Luke Campagnola's avatar
Luke Campagnola committed
281
        style = ';'.join(['%s: %s' % (k, self.labelStyle[k]) for k in self.labelStyle])
282
        
283
        return asUnicode("<span style='%s'>%s</span>") % (style, asUnicode(s))
Luke Campagnola's avatar
Luke Campagnola committed
284
285
286
287
288
289
290
    
    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
291
            if mx > self.textWidth or mx < self.textWidth-10:
Luke Campagnola's avatar
Luke Campagnola committed
292
293
294
295
296
297
                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
298
            if mx > self.textHeight or mx < self.textHeight-10:
Luke Campagnola's avatar
Luke Campagnola committed
299
300
301
302
303
304
305
306
307
308
309
                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()
    
310
    def setHeight(self, h=None):
Luke Campagnola's avatar
Luke Campagnola committed
311
312
        """Set the height of this axis reserved for ticks and tick labels.
        The height of the axis label is automatically added."""
313
        if h is None:
314
315
316
317
318
319
            if not self.style['showValues']:
                h = 0
            elif self.style['autoExpandTextSpace'] is True:
                h = self.textHeight
            else:
                h = self.style['tickTextHeight']
Luke Campagnola's avatar
Luke Campagnola committed
320
321
            h += self.style['tickTextOffset'][1] if self.style['showValues'] else 0
            h += max(0, self.style['tickLength'])
322
            if self.label.isVisible():
Luke Campagnola's avatar
Luke Campagnola committed
323
                h += self.label.boundingRect().height() * 0.8
324
325
        self.setMaximumHeight(h)
        self.setMinimumHeight(h)
Luke Campagnola's avatar
Luke Campagnola committed
326
        self.picture = None
327
328
        
    def setWidth(self, w=None):
Luke Campagnola's avatar
Luke Campagnola committed
329
330
        """Set the width of this axis reserved for ticks and tick labels.
        The width of the axis label is automatically added."""
331
        if w is None:
332
333
334
335
336
337
            if not self.style['showValues']:
                w = 0
            elif self.style['autoExpandTextSpace'] is True:
                w = self.textWidth
            else:
                w = self.style['tickTextWidth']
Luke Campagnola's avatar
Luke Campagnola committed
338
339
            w += self.style['tickTextOffset'][0] if self.style['showValues'] else 0
            w += max(0, self.style['tickLength'])
340
            if self.label.isVisible():
Luke Campagnola's avatar
Luke Campagnola committed
341
                w += self.label.boundingRect().height() * 0.8  ## bounding rect is usually an overestimate
342
343
        self.setMaximumWidth(w)
        self.setMinimumWidth(w)
Luke Campagnola's avatar
Luke Campagnola committed
344
        self.picture = None
345
        
346
347
    def pen(self):
        if self._pen is None:
348
349
            return fn.mkPen(getConfigOption('foreground'))
        return fn.mkPen(self._pen)
350
        
351
    def setPen(self, *args, **kwargs):
352
353
        """
        Set the pen used for drawing text, axes, ticks, and grid lines.
354
355
        If no arguments are given, the default foreground color will be used 
        (see :func:`setConfigOption <pyqtgraph.setConfigOption>`).
356
        """
Luke Campagnola's avatar
Luke Campagnola committed
357
        self.picture = None
358
359
360
361
        if args or kwargs:
            self._pen = fn.mkPen(*args, **kwargs)
        else:
            self._pen = fn.mkPen(getConfigOption('foreground'))
362
        self.labelStyle['color'] = '#' + fn.colorStr(self._pen.color())[:6]
Luke Campagnola's avatar
Luke Campagnola committed
363
        self.setLabel()
364
365
366
367
        self.update()
        
    def setScale(self, scale=None):
        """
368
        Set the value scaling for this axis. 
Luke Campagnola's avatar
Luke Campagnola committed
369
        
370
371
372
        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.
373
        """
374
375
376
377
378
        # Deprecated usage, kept for backward compatibility
        if scale is None:  
            scale = 1.0
            self.enableAutoSIPrefix(True)
            
379
380
381
        if scale != self.scale:
            self.scale = scale
            self.setLabel()
Luke Campagnola's avatar
Luke Campagnola committed
382
            self.picture = None
383
384
            self.update()
        
385
    def enableAutoSIPrefix(self, enable=True):
386
387
388
389
390
391
392
393
394
395
396
397
398
399
        """
        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.
        """
400
        self.autoSIPrefix = enable
Luke Campagnola's avatar
Luke Campagnola committed
401
        self.updateAutoSIPrefix()
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
        
    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()
        
        
418
    def setRange(self, mn, mx):
419
420
        """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
421
        if any(np.isinf((mn, mx))) or any(np.isnan((mn, mx))):
422
423
            raise Exception("Not setting range to [%s, %s]" % (str(mn), str(mx)))
        self.range = [mn, mx]
424
425
        if self.autoSIPrefix:
            self.updateAutoSIPrefix()
Luke Campagnola's avatar
Luke Campagnola committed
426
        self.picture = None
427
428
        self.update()
        
Luke Campagnola's avatar
Luke Campagnola committed
429
430
431
432
433
434
435
    def linkedView(self):
        """Return the ViewBox this axis is linked to"""
        if self._linkedView is None:
            return None
        else:
            return self._linkedView()
        
436
    def linkToView(self, view):
Luke Campagnola's avatar
Luke Campagnola committed
437
        """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
438
439
        oldView = self.linkedView()
        self._linkedView = weakref.ref(view)
440
        if self.orientation in ['right', 'left']:
Luke Campagnola's avatar
Luke Campagnola committed
441
442
            if oldView is not None:
                oldView.sigYRangeChanged.disconnect(self.linkedViewChanged)
443
444
            view.sigYRangeChanged.connect(self.linkedViewChanged)
        else:
Luke Campagnola's avatar
Luke Campagnola committed
445
446
            if oldView is not None:
                oldView.sigXRangeChanged.disconnect(self.linkedViewChanged)
447
448
            view.sigXRangeChanged.connect(self.linkedViewChanged)
        
Luke Campagnola's avatar
Luke Campagnola committed
449
450
451
452
453
        if oldView is not None:
            oldView.sigResized.disconnect(self.linkedViewChanged)
        view.sigResized.connect(self.linkedViewChanged)
        
    def linkedViewChanged(self, view, newRange=None):
454
        if self.orientation in ['right', 'left']:
Luke Campagnola's avatar
Luke Campagnola committed
455
456
            if newRange is None:
                newRange = view.viewRange()[1]
457
458
459
460
            if view.yInverted():
                self.setRange(*newRange[::-1])
            else:
                self.setRange(*newRange)
Luke Campagnola's avatar
Luke Campagnola committed
461
        else:
Luke Campagnola's avatar
Luke Campagnola committed
462
463
            if newRange is None:
                newRange = view.viewRange()[0]
464
465
466
467
            if view.xInverted():
                self.setRange(*newRange[::-1])
            else:
                self.setRange(*newRange)
468
469
        
    def boundingRect(self):
Luke Campagnola's avatar
Luke Campagnola committed
470
471
        linkedView = self.linkedView()
        if linkedView is None or self.grid is False:
472
473
            rect = self.mapRectFromParent(self.geometry())
            ## extend rect if ticks go in negative direction
474
            ## also extend to account for text that flows past the edges
Luke Campagnola's avatar
Luke Campagnola committed
475
            tl = self.style['tickLength']
476
            if self.orientation == 'left':
Luke Campagnola's avatar
Luke Campagnola committed
477
                rect = rect.adjusted(0, -15, -min(0,tl), 15)
478
            elif self.orientation == 'right':
Luke Campagnola's avatar
Luke Campagnola committed
479
                rect = rect.adjusted(min(0,tl), -15, 0, 15)
480
            elif self.orientation == 'top':
Luke Campagnola's avatar
Luke Campagnola committed
481
                rect = rect.adjusted(-15, 0, 15, -min(0,tl))
482
            elif self.orientation == 'bottom':
Luke Campagnola's avatar
Luke Campagnola committed
483
                rect = rect.adjusted(-15, min(0,tl), 15, 0)
484
485
            return rect
        else:
Luke Campagnola's avatar
Luke Campagnola committed
486
            return self.mapRectFromParent(self.geometry()) | linkedView.mapRectToItem(self, linkedView.boundingRect())
487
488
        
    def paint(self, p, opt, widget):
489
        profiler = debug.Profiler()
Luke Campagnola's avatar
Luke Campagnola committed
490
491
        if self.picture is None:
            try:
Luke Campagnola's avatar
Luke Campagnola committed
492
493
494
                picture = QtGui.QPicture()
                painter = QtGui.QPainter(picture)
                specs = self.generateDrawSpecs(painter)
495
                profiler('generate specs')
496
497
                if specs is not None:
                    self.drawPicture(painter, *specs)
498
                    profiler('draw picture')
Luke Campagnola's avatar
Luke Campagnola committed
499
500
            finally:
                painter.end()
Luke Campagnola's avatar
Luke Campagnola committed
501
            self.picture = picture
Luke Campagnola's avatar
Luke Campagnola committed
502
503
        #p.setRenderHint(p.Antialiasing, False)   ## Sometimes we get a segfault here ???
        #p.setRenderHint(p.TextAntialiasing, True)
Luke Campagnola's avatar
Luke Campagnola committed
504
        self.picture.play(p)
Luke Campagnola's avatar
Luke Campagnola committed
505

506
507
508
509
    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
510

511
512
513
514
515
516
517
518
519
520
521
522
            [
                [ (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()
    
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
    def setTickSpacing(self, major=None, minor=None, levels=None):
        """
        Explicitly determine the spacing of major and minor ticks. This 
        overrides the default behavior of the tickSpacing method, and disables
        the effect of setTicks(). Arguments may be either *major* and *minor*, 
        or *levels* which is a list of (spacing, offset) tuples for each 
        tick level desired.
        
        If no arguments are given, then the default behavior of tickSpacing
        is enabled.
        
        Examples::
        
            # two levels, all offsets = 0
            axis.setTickSpacing(5, 1)
            # three levels, all offsets = 0
            axis.setTickSpacing([(3, 0), (1, 0), (0.25, 0)])
            # reset to default
            axis.setTickSpacing()
        """
        
        if levels is None:
            if major is None:
                levels = None
            else:
                levels = [(major, 0), (minor, 0)]
        self._tickSpacing = levels
        self.picture = None
        self.update()
        

Luke Campagnola's avatar
Luke Campagnola committed
554
555
556
557
558
559
    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.
        
560
        The return value must be a list of tuples, one for each set of ticks::
Luke Campagnola's avatar
Luke Campagnola committed
561
        
Luke Campagnola's avatar
Luke Campagnola committed
562
563
564
565
566
567
568
            [
                (major tick spacing, offset),
                (minor tick spacing, offset),
                (sub-minor tick spacing, offset),
                ...
            ]
        """
569
570
571
572
        # First check for override tick spacing
        if self._tickSpacing is not None:
            return self._tickSpacing
        
Luke Campagnola's avatar
Luke Campagnola committed
573
574
575
576
577
        dif = abs(maxVal - minVal)
        if dif == 0:
            return []
        
        ## decide optimal minor tick spacing in pixels (this is just aesthetics)
578
        optimalTickCount = max(2., np.log(size))
Luke Campagnola's avatar
Luke Campagnola committed
579
580
581
582
583
584
585
586
587
588
589
590
591
        
        ## 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
            
592
        levels = [
Luke Campagnola's avatar
Luke Campagnola committed
593
594
            (intervals[minorIndex+2], 0),
            (intervals[minorIndex+1], 0),
Luke Campagnola's avatar
Luke Campagnola committed
595
            #(intervals[minorIndex], 0)    ## Pretty, but eats up CPU
Luke Campagnola's avatar
Luke Campagnola committed
596
597
        ]
        
598
599
600
601
602
603
604
        if self.style['maxTickLevel'] >= 2:
            ## 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
605
606
607
        
        
        
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
        ##### 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
630
631
    def tickValues(self, minVal, maxVal, size):
        """
Luke Campagnola's avatar
Luke Campagnola committed
632
633
634
635
636
637
638
        Return the values and spacing of ticks to draw::
        
            [  
                (spacing, [major ticks]), 
                (spacing, [minor ticks]), 
                ... 
            ]
Luke Campagnola's avatar
Luke Campagnola committed
639
640
641
642
        
        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
643
644
        minVal, maxVal = sorted((minVal, maxVal))
        
Luke Campagnola's avatar
Luke Campagnola committed
645
646
647
648

        minVal *= self.scale  
        maxVal *= self.scale
        #size *= self.scale
649
            
Luke Campagnola's avatar
Luke Campagnola committed
650
651
        ticks = []
        tickLevels = self.tickSpacing(minVal, maxVal, size)
Luke Campagnola's avatar
Luke Campagnola committed
652
        allValues = np.array([])
Luke Campagnola's avatar
Luke Campagnola committed
653
654
655
656
657
658
659
660
        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
661
            values = (np.arange(num) * spacing + start) / self.scale
Luke Campagnola's avatar
Luke Campagnola committed
662
663
664
            ## 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.
665
            values = list(filter(lambda x: all(np.abs(allValues-x) > spacing*0.01), values) )
Luke Campagnola's avatar
Luke Campagnola committed
666
            allValues = np.concatenate([allValues, values])
Luke Campagnola's avatar
Luke Campagnola committed
667
            ticks.append((spacing/self.scale, values))
668
669
670
            
        if self.logMode:
            return self.logTickValues(minVal, maxVal, size, ticks)
Luke Campagnola's avatar
Luke Campagnola committed
671
672
673
674
675
676
677
678
679
        
        
        #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
680
            
Luke Campagnola's avatar
Luke Campagnola committed
681
682
        return ticks
    
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
    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
704
705
706
707
708
709
710
711
712
713
714
715
716
717

    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.
        """
718
719
720
        if self.logMode:
            return self.logTickStrings(values, scale, spacing)
        
Luke Campagnola's avatar
Luke Campagnola committed
721
722
723
724
725
726
727
728
729
730
        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
731
        
732
733
734
    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
735
736
    def generateDrawSpecs(self, p):
        """
737
        Calls tickValues() and tickStrings() to determine where and how ticks should
Luke Campagnola's avatar
Luke Campagnola committed
738
739
740
        be drawn, then generates from this a set of drawing commands to be 
        interpreted by drawPicture().
        """
741
742
        profiler = debug.Profiler()

743
744
745
        #bounds = self.boundingRect()
        bounds = self.mapRectFromParent(self.geometry())
        
Luke Campagnola's avatar
Luke Campagnola committed
746
747
        linkedView = self.linkedView()
        if linkedView is None or self.grid is False:
Luke Campagnola's avatar
Luke Campagnola committed
748
            tickBounds = bounds
749
        else:
Luke Campagnola's avatar
Luke Campagnola committed
750
            tickBounds = linkedView.mapRectToItem(self, linkedView.boundingRect())
751
752
753
        
        if self.orientation == 'left':
            span = (bounds.topRight(), bounds.bottomRight())
Luke Campagnola's avatar
Luke Campagnola committed
754
            tickStart = tickBounds.right()
755
756
757
758
759
            tickStop = bounds.right()
            tickDir = -1
            axis = 0
        elif self.orientation == 'right':
            span = (bounds.topLeft(), bounds.bottomLeft())
Luke Campagnola's avatar
Luke Campagnola committed
760
            tickStart = tickBounds.left()
761
762
763
764
765
            tickStop = bounds.left()
            tickDir = 1
            axis = 0
        elif self.orientation == 'top':
            span = (bounds.bottomLeft(), bounds.bottomRight())
Luke Campagnola's avatar
Luke Campagnola committed
766
            tickStart = tickBounds.bottom()
767
768
769
770
771
            tickStop = bounds.bottom()
            tickDir = -1
            axis = 1
        elif self.orientation == 'bottom':
            span = (bounds.topLeft(), bounds.topRight())
Luke Campagnola's avatar
Luke Campagnola committed
772
            tickStart = tickBounds.top()
773
774
775
            tickStop = bounds.top()
            tickDir = 1
            axis = 1
Luke Campagnola's avatar
Luke Campagnola committed
776
777
        #print tickStart, tickStop, span
        
778
        ## determine size of this item in pixels
779
        points = list(map(self.mapToDevice, span))
Luke Campagnola's avatar
Luke Campagnola committed
780
781
        if None in points:
            return
782
        lengthInPixels = Point(points[1] - points[0]).length()
Luke Campagnola's avatar
Luke Campagnola committed
783
784
        if lengthInPixels == 0:
            return
785

786
        # Determine major / minor / subminor axis ticks
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
        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)
802
        
Luke Campagnola's avatar
Luke Campagnola committed
803
804
        ## determine mapping between tick values and local coordinates
        dif = self.range[1] - self.range[0]
805
        if dif == 0:
Mikhail Terekhov's avatar
Typo    
Mikhail Terekhov committed
806
            xScale = 1
807
            offset = 0
808
        else:
809
810
811
812
813
814
            if axis == 0:
                xScale = -bounds.height() / dif
                offset = self.range[0] * xScale - bounds.height()
            else:
                xScale = bounds.width() / dif
                offset = self.range[0] * xScale
815
816
817
818
            
        xRange = [x * xScale - offset for x in self.range]
        xMin = min(xRange)
        xMax = max(xRange)
819
        
820
        profiler('init')
821
            
Luke Campagnola's avatar
Luke Campagnola committed
822
823
        tickPositions = [] # remembers positions of previously drawn ticks
        
824
        ## compute coordinates to draw ticks
825
        ## draw three different intervals, long ticks first
Luke Campagnola's avatar
Luke Campagnola committed
826
        tickSpecs = []
Luke Campagnola's avatar
Luke Campagnola committed
827
828
829
        for i in range(len(tickLevels)):
            tickPositions.append([])
            ticks = tickLevels[i][1]
830
831
        
            ## length of tick
Luke Campagnola's avatar
Luke Campagnola committed
832
            tickLength = self.style['tickLength'] / ((i*0.5)+1.0)
Luke Campagnola's avatar
Luke Campagnola committed
833
                
Luke Campagnola's avatar
Luke Campagnola committed
834
            lineAlpha = 255 / (i+1)
Luke Campagnola's avatar
Luke Campagnola committed
835
            if self.grid is not False:
Luke Campagnola's avatar
Luke Campagnola committed
836
                lineAlpha *= self.grid/255. * np.clip((0.05  * lengthInPixels / (len(ticks)+1)), 0., 1.)
837
            
Luke Campagnola's avatar
Luke Campagnola committed
838
            for v in ticks:
839
                ## determine actual position to draw this tick
Luke Campagnola's avatar
Luke Campagnola committed
840
                x = (v * xScale) - offset
841
842
843
844
845
                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
846
847
                p1 = [x, x]
                p2 = [x, x]
848
                p1[axis] = tickStart
Luke Campagnola's avatar
Luke Campagnola committed
849
850
                p2[axis] = tickStop
                if self.grid is False:
Luke Campagnola's avatar
Luke Campagnola committed
851
                    p2[axis] += tickLength*tickDir
852
853
854
855
                tickPen = self.pen()
                color = tickPen.color()
                color.setAlpha(lineAlpha)
                tickPen.setColor(color)
Luke Campagnola's avatar
Luke Campagnola committed
856
                tickSpecs.append((tickPen, Point(p1), Point(p2)))
857
        profiler('compute ticks')
858

Luke Campagnola's avatar
Luke Campagnola committed
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
        
        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
875
        textOffset = self.style['tickTextOffset'][axis]  ## spacing between axis and text
Luke Campagnola's avatar
Luke Campagnola committed
876
877
878
879
880
881
882
        #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
883
        textSize2 = 0
884
        textRects = []
Luke Campagnola's avatar
Luke Campagnola committed
885
        textSpecs = []  ## list of draw
886
887
888
889
890
        
        # If values are hidden, return early
        if not self.style['showValues']:
            return (axisSpec, tickSpecs, textSpecs)
            
891
        for i in range(min(len(tickLevels), self.style['maxTextLevel']+1)):
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
            ## Get the list of strings to display for this level
            if tickStrings is None:
                spacing, values = tickLevels[i]
                strings = self.tickStrings(values, self.autoSIPrefixScale * self.scale, spacing)
            else:
                strings = tickStrings[i]
                
            if len(strings) == 0:
                continue
            
            ## ignore strings belonging to ticks that were previously ignored
            for j in range(len(strings)):
                if tickPositions[i][j] is None:
                    strings[j] = None

            ## Measure density of text; decide whether to draw this level
            rects = []
            for s in strings:
                if s is None:
                    rects.append(None)
Luke Campagnola's avatar
Luke Campagnola committed
912
                else:
913
914
915
916
                    br = p.boundingRect(QtCore.QRectF(0, 0, 100, 100), QtCore.Qt.AlignCenter, asUnicode(s))
                    ## boundingRect is usually just a bit too large
                    ## (but this probably depends on per-font metrics?)
                    br.setHeight(br.height() * 0.8)
Luke Campagnola's avatar
Luke Campagnola committed
917
                    
918
919
920
                    rects.append(br)
                    textRects.append(rects[-1])
            
921
922
923
924
925
926
927
928
            if len(textRects) > 0:
                ## measure all text, make sure there's enough room
                if axis == 0:
                    textSize = np.sum([r.height() for r in textRects])
                    textSize2 = np.max([r.width() for r in textRects])
                else:
                    textSize = np.sum([r.width() for r in textRects])
                    textSize2 = np.max([r.height() for r in textRects])
929
            else:
930
931
                textSize = 0
                textSize2 = 0
932

933
            if i > 0:  ## always draw top level
934
935
936
937
938
939
940
941
                ## 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.
                textFillRatio = float(textSize) / lengthInPixels
                finished = False
                for nTexts, limit in self.style['textFillLimits']:
                    if len(textSpecs) >= nTexts and textFillRatio >= limit:
                        finished = True
Luke Campagnola's avatar
Luke Campagnola committed
942
                        break
943
944
945
946
947
                if finished:
                    break
            
            #spacing, values = tickLevels[best]
            #strings = self.tickStrings(values, self.scale, spacing)
948
            # Determine exactly where tick text should be drawn
949
950
951
952
953
954
955
956
957
958
959
            for j in range(len(strings)):
                vstr = strings[j]
                if vstr is None: ## this tick was ignored because it is out of bounds
                    continue
                vstr = asUnicode(vstr)
                x = tickPositions[i][j]
                #textRect = p.boundingRect(QtCore.QRectF(0, 0, 100, 100), QtCore.Qt.AlignCenter, vstr)
                textRect = rects[j]
                height = textRect.height()
                width = textRect.width()
                #self.textHeight = height
Luke Campagnola's avatar
Luke Campagnola committed
960
                offset = max(0,self.style['tickLength']) + textOffset
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
                if self.orientation == 'left':
                    textFlags = QtCore.Qt.TextDontClip|QtCore.Qt.AlignRight|QtCore.Qt.AlignVCenter
                    rect = QtCore.QRectF(tickStop-offset-width, x-(height/2), width, height)
                elif self.orientation == 'right':
                    textFlags = QtCore.Qt.TextDontClip|QtCore.Qt.AlignLeft|QtCore.Qt.AlignVCenter
                    rect = QtCore.QRectF(tickStop+offset, x-(height/2), width, height)
                elif self.orientation == 'top':
                    textFlags = QtCore.Qt.TextDontClip|QtCore.Qt.AlignCenter|QtCore.Qt.AlignBottom
                    rect = QtCore.QRectF(x-width/2., tickStop-offset-height, width, height)
                elif self.orientation == 'bottom':
                    textFlags = QtCore.Qt.TextDontClip|QtCore.Qt.AlignCenter|QtCore.Qt.AlignTop
                    rect = QtCore.QRectF(x-width/2., tickStop+offset, width, height)

                #p.setPen(self.pen())
                #p.drawText(rect, textFlags, vstr)
                textSpecs.append((rect, textFlags, vstr))
        profiler('compute text')
Luke Campagnola's avatar
Luke Campagnola committed
978
            
979
980
        ## update max text size if needed.
        self._updateMaxTextSize(textSize2)
Luke Campagnola's avatar
Luke Campagnola committed
981
982
983
984
        
        return (axisSpec, tickSpecs, textSpecs)
    
    def drawPicture(self, p, axisSpec, tickSpecs, textSpecs):
985
986
        profiler = debug.Profiler()

Luke Campagnola's avatar
Luke Campagnola committed
987
988
989
990
991
992
993
994
995
996
997
998
999
        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)
1000
1001
        profiler('draw ticks')

Luke Campagnola's avatar
Luke Campagnola committed
1002
1003
1004
1005
1006
1007
1008
        ## 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)
1009
1010
        profiler('draw text')

1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
    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
1027
1028
        if self.linkedView() is None: 
            return
1029
1030
1031
1032
1033
        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
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
        
    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)