AxisItem.py 20.7 KB
Newer Older
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
from pyqtgraph.Qt import QtGui, QtCore
import numpy as np
from pyqtgraph.Point import Point
import pyqtgraph.debug as debug
import weakref
import pyqtgraph.functions as fn
from GraphicsWidget import GraphicsWidget

__all__ = ['AxisItem']
class AxisItem(GraphicsWidget):
    def __init__(self, orientation, pen=None, linkView=None, parent=None, maxTickLength=-5, showValues=True):
        """
        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 make a grid.
Luke Campagnola's avatar
Luke Campagnola committed
16
        If maxTickLength is negative, ticks point into the plot. 
17
18
19
20
21
22
        """
        
        
        GraphicsWidget.__init__(self, parent)
        self.label = QtGui.QGraphicsTextItem(self)
        self.showValues = showValues
Luke Campagnola's avatar
Luke Campagnola committed
23
        self.picture = None
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
        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=''
        self.labelStyle = {'color': '#CCC'}
46
        self.logMode = False
47
48
49
50
51
52
53
54
55
56
57
58
        
        self.textHeight = 18
        self.tickLength = maxTickLength
        self.scale = 1.0
        self.autoScale = True
            
        self.setRange(0, 1)
        
        if pen is None:
            pen = QtGui.QPen(QtGui.QColor(100, 100, 100))
        self.setPen(pen)
        
Luke Campagnola's avatar
Luke Campagnola committed
59
        self._linkedView = None
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
        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
76
77
        self.picture = None
        self.prepareGeometryChange()
78
79
        self.update()
        
80
81
82
83
    def setLogMode(self, log):
        self.logMode = log
        self.picture = None
        self.update()
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
        
    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
110
        self.picture = None
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
        
    def showLabel(self, show=True):
        #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):
        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
135
        self.picture = None
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
        self.update()
            
    def labelString(self):
        if self.labelUnits == '':
            if self.scale == 1.0:
                units = ''
            else:
                units = u'(x%g)' % (1.0/self.scale)
        else:
            #print repr(self.labelUnitPrefix), repr(self.labelUnits)
            units = u'(%s%s)' % (self.labelUnitPrefix, self.labelUnits)
            
        s = u'%s %s' % (self.labelText, units)
        
        style = ';'.join(['%s: "%s"' % (k, self.labelStyle[k]) for k in self.labelStyle])
        
        return u"<span style='%s'>%s</span>" % (style, s)
        
    def setHeight(self, h=None):
        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
161
        self.picture = None
162
163
164
165
166
167
168
169
170
171
172
173
        
        
    def setWidth(self, w=None):
        if w is None:
            w = max(0, self.tickLength) + 40
            if self.label.isVisible():
                w += self.textHeight
        self.setMaximumWidth(w)
        self.setMinimumWidth(w)
        
    def setPen(self, pen):
        self.pen = pen
Luke Campagnola's avatar
Luke Campagnola committed
174
        self.picture = None
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
        self.update()
        
    def setScale(self, scale=None):
        """
        Set the value scaling for this axis. 
        The scaling value 1) multiplies the values displayed along the axis
        and 2) changes the way units are displayed in the label. 
        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'
        If scale is None, then it will be determined automatically based on the current 
        range displayed by the axis.
        """
        if scale is None:
            #if self.drawLabel:  ## If there is a label, then we are free to rescale the values 
            if self.label.isVisible():
                d = self.range[1] - self.range[0]
Luke Campagnola's avatar
Luke Campagnola committed
193
194
                #(scale, prefix) = fn.siScale(d / 2.)
                (scale, prefix) = fn.siScale(max(abs(self.range[0]), abs(self.range[1])))
195
196
197
198
199
200
201
202
203
204
205
                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
        
        
        if scale != self.scale:
            self.scale = scale
            self.setLabel()
Luke Campagnola's avatar
Luke Campagnola committed
206
            self.picture = None
207
208
209
210
211
212
213
214
            self.update()
        
    def setRange(self, mn, mx):
        if mn in [np.nan, np.inf, -np.inf] or mx in [np.nan, np.inf, -np.inf]:
            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
215
        self.picture = None
216
217
        self.update()
        
Luke Campagnola's avatar
Luke Campagnola committed
218
219
220
221
222
223
224
    def linkedView(self):
        """Return the ViewBox this axis is linked to"""
        if self._linkedView is None:
            return None
        else:
            return self._linkedView()
        
225
    def linkToView(self, view):
Luke Campagnola's avatar
Luke Campagnola committed
226
227
        oldView = self.linkedView()
        self._linkedView = weakref.ref(view)
228
        if self.orientation in ['right', 'left']:
Luke Campagnola's avatar
Luke Campagnola committed
229
230
            if oldView is not None:
                oldView.sigYRangeChanged.disconnect(self.linkedViewChanged)
231
232
            view.sigYRangeChanged.connect(self.linkedViewChanged)
        else:
Luke Campagnola's avatar
Luke Campagnola committed
233
234
            if oldView is not None:
                oldView.sigXRangeChanged.disconnect(self.linkedViewChanged)
235
236
237
238
239
240
            view.sigXRangeChanged.connect(self.linkedViewChanged)
        
    def linkedViewChanged(self, view, newRange):
        self.setRange(*newRange)
        
    def boundingRect(self):
Luke Campagnola's avatar
Luke Campagnola committed
241
242
        linkedView = self.linkedView()
        if linkedView is None or self.grid is False:
243
244
245
246
247
248
249
250
251
252
253
254
            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
255
            return self.mapRectFromParent(self.geometry()) | linkedView.mapRectToItem(self, linkedView.boundingRect())
256
257
        
    def paint(self, p, opt, widget):
Luke Campagnola's avatar
Luke Campagnola committed
258
259
260
261
262
263
264
265
266
        if self.picture is None:
            self.picture = QtGui.QPicture()
            painter = QtGui.QPainter(self.picture)
            try:
                self.drawPicture(painter)
            finally:
                painter.end()
        self.picture.play(p)
        
Luke Campagnola's avatar
Luke Campagnola committed
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323


    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.
        
        The return value must be a list of three tuples:
            [
                (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
        optimalTickCount = size / pixelSpacing
        if optimalTickCount < 1:
            optimalTickCount = 1
        
        ## 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
            
        return [
            (intervals[minorIndex+2], 0),
            (intervals[minorIndex+1], 0),
            (intervals[minorIndex], 0)
        ]
        

    def tickValues(self, minVal, maxVal, size):
        """
        Return the values and spacing of ticks to draw
        [  
            (spacing, [major ticks]), 
            (spacing, [minor ticks]), 
            ... 
        ]
        
        By default, this method calls tickSpacing to determine the correct tick locations.
        This is a good method to override in subclasses.
        """
324
325
326
        if self.logMode:
            return self.logTickValues(minVal, maxVal, size)
            
Luke Campagnola's avatar
Luke Campagnola committed
327
328
329
330
331
332
333
334
335
336
337
338
339
        ticks = []
        tickLevels = self.tickSpacing(minVal, maxVal, size)
        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
            ticks.append((spacing, np.arange(num) * spacing + start))
        return ticks
    
340
341
342
343
344
345
346
347
348
349
    def logTickValues(self, minVal, maxVal, size):
        v1 = int(np.floor(minVal))
        v2 = int(np.ceil(maxVal))
        major = range(v1+1, v2)
        
        minor = []
        for v in range(v1, v2):
            minor.extend(v + np.log10(np.arange(1, 10)))
        minor = filter(lambda x: x>minVal and x<maxVal, minor)
        return [(1.0, major), (None, minor)]
Luke Campagnola's avatar
Luke Campagnola committed
350
351
352
353
354
355
356
357
358
359
360
361
362
363

    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.
        """
364
365
366
        if self.logMode:
            return self.logTickStrings(values, scale, spacing)
        
Luke Campagnola's avatar
Luke Campagnola committed
367
368
369
370
371
372
373
374
375
376
        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
377
        
378
379
380
    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
381
382
    def drawPicture(self, p):
        
Luke Campagnola's avatar
Luke Campagnola committed
383
384
385
        p.setRenderHint(p.Antialiasing, False)
        p.setRenderHint(p.TextAntialiasing, True)
        
386
387
388
389
390
391
        prof = debug.Profiler("AxisItem.paint", disabled=True)
        p.setPen(self.pen)
        
        #bounds = self.boundingRect()
        bounds = self.mapRectFromParent(self.geometry())
        
Luke Campagnola's avatar
Luke Campagnola committed
392
393
        linkedView = self.linkedView()
        if linkedView is None or self.grid is False:
Luke Campagnola's avatar
Luke Campagnola committed
394
            tickBounds = bounds
395
        else:
Luke Campagnola's avatar
Luke Campagnola committed
396
            tickBounds = linkedView.mapRectToItem(self, linkedView.boundingRect())
397
398
399
        
        if self.orientation == 'left':
            span = (bounds.topRight(), bounds.bottomRight())
Luke Campagnola's avatar
Luke Campagnola committed
400
            tickStart = tickBounds.right()
401
402
403
404
405
            tickStop = bounds.right()
            tickDir = -1
            axis = 0
        elif self.orientation == 'right':
            span = (bounds.topLeft(), bounds.bottomLeft())
Luke Campagnola's avatar
Luke Campagnola committed
406
            tickStart = tickBounds.left()
407
408
409
410
411
            tickStop = bounds.left()
            tickDir = 1
            axis = 0
        elif self.orientation == 'top':
            span = (bounds.bottomLeft(), bounds.bottomRight())
Luke Campagnola's avatar
Luke Campagnola committed
412
            tickStart = tickBounds.bottom()
413
414
415
416
417
            tickStop = bounds.bottom()
            tickDir = -1
            axis = 1
        elif self.orientation == 'bottom':
            span = (bounds.topLeft(), bounds.topRight())
Luke Campagnola's avatar
Luke Campagnola committed
418
            tickStart = tickBounds.top()
419
420
421
            tickStop = bounds.top()
            tickDir = 1
            axis = 1
Luke Campagnola's avatar
Luke Campagnola committed
422
423
        #print tickStart, tickStop, span
        
424
425
        ## draw long line along axis
        p.drawLine(*span)
Luke Campagnola's avatar
Luke Campagnola committed
426
        p.translate(0.5,0)  ## resolves some damn pixel ambiguity
427
428
429
430

        ## determine size of this item in pixels
        points = map(self.mapToDevice, span)
        lengthInPixels = Point(points[1] - points[0]).length()
Luke Campagnola's avatar
Luke Campagnola committed
431
432
        if lengthInPixels == 0:
            return
433
434


Luke Campagnola's avatar
Luke Campagnola committed
435
        tickLevels = self.tickValues(self.range[0], self.range[1], lengthInPixels)
436
        
Luke Campagnola's avatar
Luke Campagnola committed
437
        textLevel = 1  ## draw text at this scale level
438
        
Luke Campagnola's avatar
Luke Campagnola committed
439
440
        ## determine mapping between tick values and local coordinates
        dif = self.range[1] - self.range[0]
441
        if axis == 0:
Luke Campagnola's avatar
Luke Campagnola committed
442
443
            xScale = -bounds.height() / dif
            offset = self.range[0] * xScale - bounds.height()
444
        else:
Luke Campagnola's avatar
Luke Campagnola committed
445
446
            xScale = bounds.width() / dif
            offset = self.range[0] * xScale
447
448
449
        
        prof.mark('init')
            
Luke Campagnola's avatar
Luke Campagnola committed
450
451
452
        tickPositions = [] # remembers positions of previously drawn ticks
        
        ## draw ticks
453
454
        ## (to improve performance, we do not interleave line and text drawing, since this causes unnecessary pipeline switching)
        ## draw three different intervals, long ticks first
Luke Campagnola's avatar
Luke Campagnola committed
455
456
457
        for i in range(len(tickLevels)):
            tickPositions.append([])
            ticks = tickLevels[i][1]
458
459
        
            ## length of tick
Luke Campagnola's avatar
Luke Campagnola committed
460
            tickLength = self.tickLength / ((i*1.0)+1.0)
Luke Campagnola's avatar
Luke Campagnola committed
461
                
Luke Campagnola's avatar
Luke Campagnola committed
462
            lineAlpha = 255 / (i+1)
Luke Campagnola's avatar
Luke Campagnola committed
463
            if self.grid is not False:
Luke Campagnola's avatar
Luke Campagnola committed
464
                lineAlpha = self.grid
465
            
Luke Campagnola's avatar
Luke Campagnola committed
466
467
468
469
            for v in ticks:
                x = (v * xScale) - offset
                p1 = [x, x]
                p2 = [x, x]
470
                p1[axis] = tickStart
Luke Campagnola's avatar
Luke Campagnola committed
471
472
                p2[axis] = tickStop
                if self.grid is False:
Luke Campagnola's avatar
Luke Campagnola committed
473
                    p2[axis] += tickLength*tickDir
Luke Campagnola's avatar
Luke Campagnola committed
474
                p.setPen(QtGui.QPen(QtGui.QColor(150, 150, 150, lineAlpha)))
Luke Campagnola's avatar
Luke Campagnola committed
475
                p.drawLine(Point(p1), Point(p2))
Luke Campagnola's avatar
Luke Campagnola committed
476
                tickPositions[i].append(x)
477
        prof.mark('draw ticks')
Luke Campagnola's avatar
Luke Campagnola committed
478
479
480
481
482
483
484
        
        ## determine level to draw text
        best = 0
        for i in range(len(tickLevels)):
            ## take a small sample of strings and measure their rendered text
            spacing, values = tickLevels[i]
            strings = self.tickStrings(values[:2], self.scale, spacing)
485
486
            if len(strings) == 0:
                continue
Luke Campagnola's avatar
Luke Campagnola committed
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
            textRects = [p.boundingRect(QtCore.QRectF(0, 0, 100, 100), QtCore.Qt.AlignCenter, s) for s in strings]
            if axis == 0:
                textSize = np.max([r.height() for r in textRects])
            else:
                textSize = np.max([r.width() for r in textRects])
                
            ## If these strings are not too crowded, then this level is ok
            textFillRatio = float(textSize * len(values)) / lengthInPixels
            if textFillRatio < 0.7:
                best = i
                continue
        prof.mark('measure text')
            
        spacing, values = tickLevels[best]
        strings = self.tickStrings(values, self.scale, spacing)
        for j in range(len(strings)):
            vstr = strings[j]
            x = tickPositions[best][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)
            
            p.setPen(QtGui.QPen(QtGui.QColor(150, 150, 150)))
            p.drawText(rect, textFlags, vstr)
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
        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
542
543
        if self.linkedView() is None: 
            return
544
545
546
547
548
        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
549
550
551
552
553
554
555
556
557
558
559
560
561
        
    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)