AxisItem.py 42.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 68 69
        # If the user specifies a width / height, remember that setting
        # indefinitely.
        self.fixedWidth = None
        self.fixedHeight = None
        
70 71 72
        self.labelText = ''
        self.labelUnits = ''
        self.labelUnitPrefix=''
Luke Campagnola's avatar
Luke Campagnola committed
73
        self.labelStyle = {}
74
        self.logMode = False
75
        self.tickFont = None
76
        
77
        self._tickLevels = None  ## used to override the automatic ticking system with explicit ticks
78
        self._tickSpacing = None  # used to override default tickSpacing method
79
        self.scale = 1.0
80 81
        self.autoSIPrefix = True
        self.autoSIPrefixScale = 1.0
82
        
83 84
        self.setRange(0, 1)
        
85 86 87 88
        if pen is None:
            self.setPen()
        else:
            self.setPen(pen)
89
        
Luke Campagnola's avatar
Luke Campagnola committed
90
        self._linkedView = None
91 92
        if linkView is not None:
            self.linkToView(linkView)
93
        
94 95 96 97
        self.showLabel(False)
        
        self.grid = False
        #self.setCacheMode(self.DeviceCoordinateCache)
Luke Campagnola's avatar
Luke Campagnola committed
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 129 130 131 132 133

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

537 538 539 540
    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
541

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

        minVal *= self.scale  
        maxVal *= self.scale
        #size *= self.scale
680
            
Luke Campagnola's avatar
Luke Campagnola committed
681 682
        ticks = []
        tickLevels = self.tickSpacing(minVal, maxVal, size)
Luke Campagnola's avatar
Luke Campagnola committed
683
        allValues = np.array([])
Luke Campagnola's avatar
Luke Campagnola committed
684 685 686 687 688 689 690 691
        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
692
            values = (np.arange(num) * spacing + start) / self.scale
Luke Campagnola's avatar
Luke Campagnola committed
693 694 695
            ## 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.
696
            values = list(filter(lambda x: all(np.abs(allValues-x) > spacing*0.01), values) )
Luke Campagnola's avatar
Luke Campagnola committed
697
            allValues = np.concatenate([allValues, values])
Luke Campagnola's avatar
Luke Campagnola committed
698
            ticks.append((spacing/self.scale, values))
699 700 701
            
        if self.logMode:
            return self.logTickValues(minVal, maxVal, size, ticks)
Luke Campagnola's avatar
Luke Campagnola committed
702 703 704 705 706 707 708 709 710
        
        
        #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
711
            
Luke Campagnola's avatar
Luke Campagnola committed
712 713
        return ticks
    
714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734
    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
735 736 737 738 739 740 741 742 743 744 745 746 747 748

    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.
        """
749 750 751
        if self.logMode:
            return self.logTickStrings(values, scale, spacing)
        
Luke Campagnola's avatar
Luke Campagnola committed
752 753 754 755 756 757 758 759 760 761
        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
762
        
763 764 765
    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
766 767
    def generateDrawSpecs(self, p):
        """
768
        Calls tickValues() and tickStrings() to determine where and how ticks should
Luke Campagnola's avatar
Luke Campagnola committed
769 770 771
        be drawn, then generates from this a set of drawing commands to be 
        interpreted by drawPicture().
        """
772 773
        profiler = debug.Profiler()

774 775 776
        #bounds = self.boundingRect()
        bounds = self.mapRectFromParent(self.geometry())
        
Luke Campagnola's avatar
Luke Campagnola committed
777 778
        linkedView = self.linkedView()
        if linkedView is None or self.grid is False:
Luke Campagnola's avatar
Luke Campagnola committed
779
            tickBounds = bounds
780
        else:
Luke Campagnola's avatar
Luke Campagnola committed
781
            tickBounds = linkedView.mapRectToItem(self, linkedView.boundingRect())
782 783 784
        
        if self.orientation == 'left':
            span = (bounds.topRight(), bounds.bottomRight())
Luke Campagnola's avatar
Luke Campagnola committed
785
            tickStart = tickBounds.right()
786 787 788 789 790
            tickStop = bounds.right()
            tickDir = -1
            axis = 0
        elif self.orientation == 'right':
            span = (bounds.topLeft(), bounds.bottomLeft())
Luke Campagnola's avatar
Luke Campagnola committed
791
            tickStart = tickBounds.left()
792 793 794 795 796
            tickStop = bounds.left()
            tickDir = 1
            axis = 0
        elif self.orientation == 'top':
            span = (bounds.bottomLeft(), bounds.bottomRight())
Luke Campagnola's avatar
Luke Campagnola committed
797
            tickStart = tickBounds.bottom()
798 799 800 801 802
            tickStop = bounds.bottom()
            tickDir = -1
            axis = 1
        elif self.orientation == 'bottom':
            span = (bounds.topLeft(), bounds.topRight())
Luke Campagnola's avatar
Luke Campagnola committed
803
            tickStart = tickBounds.top()
804 805 806
            tickStop = bounds.top()
            tickDir = 1
            axis = 1
Luke Campagnola's avatar
Luke Campagnola committed
807 808
        #print tickStart, tickStop, span
        
809
        ## determine size of this item in pixels
810
        points = list(map(self.mapToDevice, span))
Luke Campagnola's avatar
Luke Campagnola committed
811 812
        if None in points:
            return
813
        lengthInPixels = Point(points[1] - points[0]).length()
Luke Campagnola's avatar
Luke Campagnola committed
814 815
        if lengthInPixels == 0:
            return
816

817
        # Determine major / minor / subminor axis ticks
818 819 820 821 822 823 824 825 826 827 828 829 830 831 832
        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)
833
        
Luke Campagnola's avatar
Luke Campagnola committed
834 835
        ## determine mapping between tick values and local coordinates
        dif = self.range[1] - self.range[0]
836
        if dif == 0:
Mikhail Terekhov's avatar
Typo  
Mikhail Terekhov committed
837
            xScale = 1
838
            offset = 0
839
        else:
840 841 842 843 844 845
            if axis == 0:
                xScale = -bounds.height() / dif
                offset = self.range[0] * xScale - bounds.height()
            else:
                xScale = bounds.width() / dif
                offset = self.range[0] * xScale
846 847 848 849
            
        xRange = [x * xScale - offset for x in self.range]
        xMin = min(xRange)
        xMax = max(xRange)
850
        
851
        profiler('init')
852
            
Luke Campagnola's avatar
Luke Campagnola committed
853 854
        tickPositions = [] # remembers positions of previously drawn ticks
        
855
        ## compute coordinates to draw ticks
856
        ## draw three different intervals, long ticks first
Luke Campagnola's avatar
Luke Campagnola committed
857
        tickSpecs = []
Luke Campagnola's avatar
Luke Campagnola committed
858 859 860
        for i in range(len(tickLevels)):
            tickPositions.append([])
            ticks = tickLevels[i][1]
861 862
        
            ## length of tick
Luke Campagnola's avatar
Luke Campagnola committed
863
            tickLength = self.style['tickLength'] / ((i*0.5)+1.0)
Luke Campagnola's avatar
Luke Campagnola committed
864
                
Luke Campagnola's avatar
Luke Campagnola committed
865
            lineAlpha = 255 / (i+1)
Luke Campagnola's avatar
Luke Campagnola committed
866
            if self.grid is not False:
Luke Campagnola's avatar
Luke Campagnola committed
867
                lineAlpha *= self.grid/255. * np.clip((0.05  * lengthInPixels / (len(ticks)+1)), 0., 1.)
868
            
Luke Campagnola's avatar
Luke Campagnola committed
869
            for v in ticks:
870
                ## determine actual position to draw this tick
Luke Campagnola's avatar
Luke Campagnola committed
871
                x = (v * xScale) - offset
872 873 874 875 876
                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
877 878
                p1 = [x, x]
                p2 = [x, x]
879
                p1[axis] = tickStart
Luke Campagnola's avatar
Luke Campagnola committed
880 881
                p2[axis] = tickStop
                if self.grid is False:
Luke Campagnola's avatar
Luke Campagnola committed
882
                    p2[axis] += tickLength*tickDir
883 884 885 886
                tickPen = self.pen()
                color = tickPen.color()
                color.setAlpha(lineAlpha)
                tickPen.setColor(color)
Luke Campagnola's avatar
Luke Campagnola committed
887
                tickSpecs.append((tickPen, Point(p1), Point(p2)))
888
        profiler('compute ticks')
889

Luke Campagnola's avatar
Luke Campagnola committed
890 891 892 893 894 895 896 897 898 899 900 901 902 903 904 905
        
        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
906
        textOffset = self.style['tickTextOffset'][axis]  ## spacing between axis and text
Luke Campagnola's avatar
Luke Campagnola committed
907 908 909 910 911 912 913
        #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
914
        textSize2 = 0
915
        textRects = []
Luke Campagnola's avatar
Luke Campagnola committed
916
        textSpecs = []  ## list of draw
917 918 919 920 921
        
        # If values are hidden, return early
        if not self.style['showValues']:
            return (axisSpec, tickSpecs, textSpecs)
            
922
        for i in range(min(len(tickLevels), self.style['maxTextLevel']+1)):
923 924 925 926 927 928 929 930 931 932 933 934 935 936 937 938 939 940 941 942
            ## 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
943
                else:
944 945 946 947
                    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
948
                    
949 950 951
                    rects.append(br)
                    textRects.append(rects[-1])
            
952 953 954 955 956 957 958 959
            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])
960
            else:
961 962
                textSize = 0
                textSize2 = 0
963

964
            if i > 0:  ## always draw top level
965 966 967 968 969 970 971 972
                ## 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
973
                        break
974 975 976 977 978
                if finished:
                    break
            
            #spacing, values = tickLevels[best]
            #strings = self.tickStrings(values, self.scale, spacing)
979
            # Determine exactly where tick text should be drawn
980 981 982 983 984 985 986 987 988 989 990
            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
991
                offset = max(0,self.style['tickLength']) + textOffset
992 993 994 995 996 997 998 999 1000 1001 1002 1003 1004 1005 1006 1007 1008
                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
1009
            
1010 1011
        ## update max text size if needed.
        self._updateMaxTextSize(textSize2)
Luke Campagnola's avatar
Luke Campagnola committed
1012 1013 1014 1015
        
        return (axisSpec, tickSpecs, textSpecs)
    
    def drawPicture(self, p, axisSpec, tickSpecs, textSpecs):
1016 1017
        profiler = debug.Profiler()

Luke Campagnola's avatar
Luke Campagnola committed
1018 1019 1020