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

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

315 316 317 318
    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
319

320 321 322 323 324 325 326 327 328 329 330 331
            [
                [ (majorTickValue1, majorTickString1), (majorTickValue2, majorTickString2), ... ],
                [ (minorTickValue1, minorTickString1), (minorTickValue2, minorTickString2), ... ],
                ...
            ]
        
        If *ticks* is None, then the default tick system will be used instead.
        """
        self._tickLevels = ticks
        self.picture = None
        self.update()
    
Luke Campagnola's avatar
Luke Campagnola committed
332 333 334 335 336 337
    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.
        
Luke Campagnola's avatar
Luke Campagnola committed
338 339
        The return value must be a list of three tuples::
        
Luke Campagnola's avatar
Luke Campagnola committed
340 341 342 343 344 345 346 347 348 349 350 351 352
            [
                (major tick spacing, offset),
                (minor tick spacing, offset),
                (sub-minor tick spacing, offset),
                ...
            ]
        """
        dif = abs(maxVal - minVal)
        if dif == 0:
            return []
        
        ## decide optimal minor tick spacing in pixels (this is just aesthetics)
        pixelSpacing = np.log(size+10) * 5
353
        optimalTickCount = max(2., size / pixelSpacing)
Luke Campagnola's avatar
Luke Campagnola committed
354 355 356 357 358 359 360 361 362 363 364 365 366
        
        ## 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
            
367
        levels = [
Luke Campagnola's avatar
Luke Campagnola committed
368 369
            (intervals[minorIndex+2], 0),
            (intervals[minorIndex+1], 0),
Luke Campagnola's avatar
Luke Campagnola committed
370
            #(intervals[minorIndex], 0)    ## Pretty, but eats up CPU
Luke Campagnola's avatar
Luke Campagnola committed
371 372
        ]
        
373 374 375 376 377 378 379 380 381
        ## 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
        
        
        
382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404
        ##### 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
405 406 407

    def tickValues(self, minVal, maxVal, size):
        """
Luke Campagnola's avatar
Luke Campagnola committed
408 409 410 411 412 413 414
        Return the values and spacing of ticks to draw::
        
            [  
                (spacing, [major ticks]), 
                (spacing, [minor ticks]), 
                ... 
            ]
Luke Campagnola's avatar
Luke Campagnola committed
415 416 417 418
        
        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
419 420
        minVal, maxVal = sorted((minVal, maxVal))
        
421
            
Luke Campagnola's avatar
Luke Campagnola committed
422 423
        ticks = []
        tickLevels = self.tickSpacing(minVal, maxVal, size)
Luke Campagnola's avatar
Luke Campagnola committed
424
        allValues = np.array([])
Luke Campagnola's avatar
Luke Campagnola committed
425 426 427 428 429 430 431 432
        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
433
            values = np.arange(num) * spacing + start
Luke Campagnola's avatar
Luke Campagnola committed
434 435 436
            ## 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.
437
            values = list(filter(lambda x: all(np.abs(allValues-x) > spacing*0.01), values) )
Luke Campagnola's avatar
Luke Campagnola committed
438
            allValues = np.concatenate([allValues, values])
439
            ticks.append((spacing, values))
440 441 442 443
            
        if self.logMode:
            return self.logTickValues(minVal, maxVal, size, ticks)
            
Luke Campagnola's avatar
Luke Campagnola committed
444 445
        return ticks
    
446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466
    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
467 468 469 470 471 472 473 474 475 476 477 478 479 480

    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.
        """
481 482 483
        if self.logMode:
            return self.logTickStrings(values, scale, spacing)
        
Luke Campagnola's avatar
Luke Campagnola committed
484 485 486 487 488 489 490 491 492 493
        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
494
        
495 496 497
    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
498 499
    def drawPicture(self, p):
        
Luke Campagnola's avatar
Luke Campagnola committed
500 501 502
        p.setRenderHint(p.Antialiasing, False)
        p.setRenderHint(p.TextAntialiasing, True)
        
503 504 505 506 507
        prof = debug.Profiler("AxisItem.paint", disabled=True)
        
        #bounds = self.boundingRect()
        bounds = self.mapRectFromParent(self.geometry())
        
Luke Campagnola's avatar
Luke Campagnola committed
508 509
        linkedView = self.linkedView()
        if linkedView is None or self.grid is False:
Luke Campagnola's avatar
Luke Campagnola committed
510
            tickBounds = bounds
511
        else:
Luke Campagnola's avatar
Luke Campagnola committed
512
            tickBounds = linkedView.mapRectToItem(self, linkedView.boundingRect())
513 514 515
        
        if self.orientation == 'left':
            span = (bounds.topRight(), bounds.bottomRight())
Luke Campagnola's avatar
Luke Campagnola committed
516
            tickStart = tickBounds.right()
517 518 519 520 521
            tickStop = bounds.right()
            tickDir = -1
            axis = 0
        elif self.orientation == 'right':
            span = (bounds.topLeft(), bounds.bottomLeft())
Luke Campagnola's avatar
Luke Campagnola committed
522
            tickStart = tickBounds.left()
523 524 525 526 527
            tickStop = bounds.left()
            tickDir = 1
            axis = 0
        elif self.orientation == 'top':
            span = (bounds.bottomLeft(), bounds.bottomRight())
Luke Campagnola's avatar
Luke Campagnola committed
528
            tickStart = tickBounds.bottom()
529 530 531 532 533
            tickStop = bounds.bottom()
            tickDir = -1
            axis = 1
        elif self.orientation == 'bottom':
            span = (bounds.topLeft(), bounds.topRight())
Luke Campagnola's avatar
Luke Campagnola committed
534
            tickStart = tickBounds.top()
535 536 537
            tickStop = bounds.top()
            tickDir = 1
            axis = 1
Luke Campagnola's avatar
Luke Campagnola committed
538 539
        #print tickStart, tickStop, span
        
540
        ## draw long line along axis
541
        p.setPen(self.pen())
542
        p.drawLine(*span)
Luke Campagnola's avatar
Luke Campagnola committed
543
        p.translate(0.5,0)  ## resolves some damn pixel ambiguity
544 545

        ## determine size of this item in pixels
546
        points = list(map(self.mapToDevice, span))
Luke Campagnola's avatar
Luke Campagnola committed
547 548
        if None in points:
            return
549
        lengthInPixels = Point(points[1] - points[0]).length()
Luke Campagnola's avatar
Luke Campagnola committed
550 551
        if lengthInPixels == 0:
            return
552

553 554 555 556 557 558 559 560 561 562 563 564 565 566 567
        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)
568
        
Luke Campagnola's avatar
Luke Campagnola committed
569
        textLevel = 1  ## draw text at this scale level
570
        
Luke Campagnola's avatar
Luke Campagnola committed
571 572
        ## determine mapping between tick values and local coordinates
        dif = self.range[1] - self.range[0]
573
        if axis == 0:
Luke Campagnola's avatar
Luke Campagnola committed
574 575
            xScale = -bounds.height() / dif
            offset = self.range[0] * xScale - bounds.height()
576
        else:
Luke Campagnola's avatar
Luke Campagnola committed
577 578
            xScale = bounds.width() / dif
            offset = self.range[0] * xScale
579 580 581 582
            
        xRange = [x * xScale - offset for x in self.range]
        xMin = min(xRange)
        xMax = max(xRange)
583 584 585
        
        prof.mark('init')
            
Luke Campagnola's avatar
Luke Campagnola committed
586 587 588
        tickPositions = [] # remembers positions of previously drawn ticks
        
        ## draw ticks
589 590
        ## (to improve performance, we do not interleave line and text drawing, since this causes unnecessary pipeline switching)
        ## draw three different intervals, long ticks first
591
        
Luke Campagnola's avatar
Luke Campagnola committed
592 593 594
        for i in range(len(tickLevels)):
            tickPositions.append([])
            ticks = tickLevels[i][1]
595 596
        
            ## length of tick
597
            tickLength = self.tickLength / ((i*0.5)+1.0)
Luke Campagnola's avatar
Luke Campagnola committed
598
                
Luke Campagnola's avatar
Luke Campagnola committed
599
            lineAlpha = 255 / (i+1)
Luke Campagnola's avatar
Luke Campagnola committed
600
            if self.grid is not False:
Luke Campagnola's avatar
Luke Campagnola committed
601
                lineAlpha *= self.grid/255. * np.clip((0.05  * lengthInPixels / (len(ticks)+1)), 0., 1.)
602
            
Luke Campagnola's avatar
Luke Campagnola committed
603
            for v in ticks:
604
                ## determine actual position to draw this tick
Luke Campagnola's avatar
Luke Campagnola committed
605
                x = (v * xScale) - offset
606 607 608 609 610
                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
611 612
                p1 = [x, x]
                p2 = [x, x]
613
                p1[axis] = tickStart
Luke Campagnola's avatar
Luke Campagnola committed
614 615
                p2[axis] = tickStop
                if self.grid is False:
Luke Campagnola's avatar
Luke Campagnola committed
616
                    p2[axis] += tickLength*tickDir
617 618 619 620 621
                tickPen = self.pen()
                color = tickPen.color()
                color.setAlpha(lineAlpha)
                tickPen.setColor(color)
                p.setPen(tickPen)
Luke Campagnola's avatar
Luke Campagnola committed
622
                p.drawLine(Point(p1), Point(p2))
623
        prof.mark('draw ticks')
624 625 626

        ## Draw text until there is no more room (or no more text)
        textRects = []
Luke Campagnola's avatar
Luke Campagnola committed
627
        for i in range(len(tickLevels)):
628 629 630 631
            ## Get the list of strings to display for this level
            if tickStrings is None:
                spacing, values = tickLevels[i]
                strings = self.tickStrings(values, self.scale, spacing)
Luke Campagnola's avatar
Luke Campagnola committed
632
            else:
633
                strings = tickStrings[i]
Luke Campagnola's avatar
Luke Campagnola committed
634
                
635
            if len(strings) == 0:
Luke Campagnola's avatar
Luke Campagnola committed
636
                continue
637 638 639 640 641
            
            ## ignore strings belonging to ticks that were previously ignored
            for j in range(len(strings)):
                if tickPositions[i][j] is None:
                    strings[j] = None
642

643
            textRects.extend([p.boundingRect(QtCore.QRectF(0, 0, 100, 100), QtCore.Qt.AlignCenter, s) for s in strings if s is not None])
644 645 646 647 648 649 650 651 652 653 654 655 656 657 658
            if i > 0:  ## always draw top level
                ## measure all text, make sure there's enough room
                if axis == 0:
                    textSize = np.sum([r.height() for r in textRects])
                else:
                    textSize = np.sum([r.width() for r in textRects])

                ## If the strings are too crowded, stop drawing text now
                textFillRatio = float(textSize) / lengthInPixels
                if textFillRatio > 0.7:
                    break
            #spacing, values = tickLevels[best]
            #strings = self.tickStrings(values, self.scale, spacing)
            for j in range(len(strings)):
                vstr = strings[j]
659 660
                if vstr is None:## this tick was ignored because it is out of bounds
                    continue
661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677
                x = tickPositions[i][j]
                textRect = p.boundingRect(QtCore.QRectF(0, 0, 100, 100), QtCore.Qt.AlignCenter, vstr)
                height = textRect.height()
                self.textHeight = height
                if self.orientation == 'left':
                    textFlags = QtCore.Qt.AlignRight|QtCore.Qt.AlignVCenter
                    rect = QtCore.QRectF(tickStop-100, x-(height/2), 99-max(0,self.tickLength), height)
                elif self.orientation == 'right':
                    textFlags = QtCore.Qt.AlignLeft|QtCore.Qt.AlignVCenter
                    rect = QtCore.QRectF(tickStop+max(0,self.tickLength)+1, x-(height/2), 100-max(0,self.tickLength), height)
                elif self.orientation == 'top':
                    textFlags = QtCore.Qt.AlignCenter|QtCore.Qt.AlignBottom
                    rect = QtCore.QRectF(x-100, tickStop-max(0,self.tickLength)-height, 200, height)
                elif self.orientation == 'bottom':
                    textFlags = QtCore.Qt.AlignCenter|QtCore.Qt.AlignTop
                    rect = QtCore.QRectF(x-100, tickStop+max(0,self.tickLength), 200, height)

678
                p.setPen(self.pen())
679
                p.drawText(rect, textFlags, vstr)
680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698
        prof.mark('draw text')
        prof.finish()
        
    def show(self):
        
        if self.orientation in ['left', 'right']:
            self.setWidth()
        else:
            self.setHeight()
        GraphicsWidget.show(self)
        
    def hide(self):
        if self.orientation in ['left', 'right']:
            self.setWidth(0)
        else:
            self.setHeight(0)
        GraphicsWidget.hide(self)

    def wheelEvent(self, ev):
Luke Campagnola's avatar
Luke Campagnola committed
699 700
        if self.linkedView() is None: 
            return
701 702 703 704 705
        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
706 707 708 709 710 711 712 713 714 715 716 717 718
        
    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)