ViewBox.py 62.6 KB
Newer Older
1
from pyqtgraph.Qt import QtGui, QtCore
Luke Campagnola's avatar
Luke Campagnola committed
2
from pyqtgraph.python2_3 import sortList
3
4
5
6
7
8
9
10
11
import numpy as np
from pyqtgraph.Point import Point
import pyqtgraph.functions as fn
from .. ItemGroup import ItemGroup
from .. GraphicsWidget import GraphicsWidget
from pyqtgraph.GraphicsScene import GraphicsScene
import pyqtgraph
import weakref
from copy import deepcopy
Luke Campagnola's avatar
Luke Campagnola committed
12
import pyqtgraph.debug as debug
13
14
15
16
17
18
19

__all__ = ['ViewBox']


class ChildGroup(ItemGroup):
    
    sigItemsChanged = QtCore.Signal()
20
21
22
23
    def __init__(self, parent):
        ItemGroup.__init__(self, parent)
        # excempt from telling view when transform changes
        self._GraphicsObject__inform_view_on_change = False
24
25
26
27
28
29
30
31
32
33
34
    
    def itemChange(self, change, value):
        ret = ItemGroup.itemChange(self, change, value)
        if change == self.ItemChildAddedChange or change == self.ItemChildRemovedChange:
            self.sigItemsChanged.emit()
        
        return ret


class ViewBox(GraphicsWidget):
    """
Luke Campagnola's avatar
Luke Campagnola committed
35
36
    **Bases:** :class:`GraphicsWidget <pyqtgraph.GraphicsWidget>`
    
37
    Box that allows internal scaling/panning of children by mouse drag. 
Luke Campagnola's avatar
Luke Campagnola committed
38
39
40
41
42
43
44
45
46
    This class is usually created automatically as part of a :class:`PlotItem <pyqtgraph.PlotItem>` or :class:`Canvas <pyqtgraph.canvas.Canvas>` or with :func:`GraphicsLayout.addViewBox() <pyqtgraph.GraphicsLayout.addViewBox>`.
    
    Features:
    
        - Scaling contents by mouse or auto-scale when contents change
        - View linking--multiple views display the same data ranges
        - Configurable by context menu
        - Item coordinate mapping methods
    
47
48
49
50
51
52
53
54
55
    Not really compatible with GraphicsView having the same functionality.
    """
    
    sigYRangeChanged = QtCore.Signal(object, object)
    sigXRangeChanged = QtCore.Signal(object, object)
    sigRangeChangedManually = QtCore.Signal(object)
    sigRangeChanged = QtCore.Signal(object, object)
    #sigActionPositionChanged = QtCore.Signal(object)
    sigStateChanged = QtCore.Signal(object)
56
    sigTransformChanged = QtCore.Signal(object)
Luke Campagnola's avatar
Luke Campagnola committed
57
    sigResized = QtCore.Signal(object)
58
59
60
61
62
63
64
65
66
67
68
69
70
71
    
    ## mouse modes
    PanMode = 3
    RectMode = 1
    
    ## axes
    XAxis = 0
    YAxis = 1
    XYAxes = 2
    
    ## for linking views together
    NamedViews = weakref.WeakValueDictionary()   # name: ViewBox
    AllViews = weakref.WeakKeyDictionary()       # ViewBox: None
    
72
    def __init__(self, parent=None, border=None, lockAspect=False, enableMouse=True, invertY=False, enableMenu=True, name=None):
Luke Campagnola's avatar
Luke Campagnola committed
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
        """
        =============  =============================================================
        **Arguments**
        *parent*       (QGraphicsWidget) Optional parent widget
        *border*       (QPen) Do draw a border around the view, give any 
                       single argument accepted by :func:`mkPen <pyqtgraph.mkPen>`
        *lockAspect*   (False or float) The aspect ratio to lock the view 
                       coorinates to. (or False to allow the ratio to change)
        *enableMouse*  (bool) Whether mouse can be used to scale/pan the view 
        *invertY*      (bool) See :func:`invertY <pyqtgraph.ViewBox.invertY>`
        =============  =============================================================
        """
        
        
        
88
89
90
91
92
93
        GraphicsWidget.__init__(self, parent)
        self.name = None
        self.linksBlocked = False
        self.addedItems = []
        #self.gView = view
        #self.showGrid = showGrid
94
95
        self._matrixNeedsUpdate = True  ## indicates that range has changed, but matrix update was deferred
        self._autoRangeNeedsUpdate = True ## indicates auto-range needs to be recomputed.
96
97
98
99
100
101
102
103
104
105
106
107
        
        self.state = {
            
            ## separating targetRange and viewRange allows the view to be resized
            ## while keeping all previously viewed contents visible
            'targetRange': [[0,1], [0,1]],   ## child coord. range visible [[xmin, xmax], [ymin, ymax]]
            'viewRange': [[0,1], [0,1]],     ## actual range viewed
        
            'yInverted': invertY,
            'aspectLocked': False,    ## False if aspect is unlocked, otherwise float specifies the locked ratio.
            'autoRange': [True, True],  ## False if auto range is disabled, 
                                          ## otherwise float gives the fraction of data that is visible
108
109
            'autoPan': [False, False],         ## whether to only pan (do not change scaling) when auto-range is enabled
            'autoVisibleOnly': [False, False], ## whether to auto-range only to the visible portion of a plot 
110
111
            'linkedViews': [None, None],  ## may be None, "viewName", or weakref.ref(view)
                                          ## a name string indicates that the view *should* link to another, but no view with that name exists yet.
112
113
114
            
            'mouseEnabled': [enableMouse, enableMouse],
            'mouseMode': ViewBox.PanMode if pyqtgraph.getConfigOption('leftButtonPan') else ViewBox.RectMode,  
115
            'enableMenu': enableMenu,
116
            'wheelScaleFactor': -1.0 / 8.0,
117
118

            'background': None,
119
        }
120
        self._updatingRange = False  ## Used to break recursive loops. See updateAutoRange.
Luke Campagnola's avatar
Luke Campagnola committed
121
        self._itemBoundsCache = weakref.WeakKeyDictionary()
122
        
Luke Campagnola's avatar
Luke Campagnola committed
123
        self.locateGroup = None  ## items displayed when using ViewBox.locate(item)
124
125
126
127
128
        
        self.setFlag(self.ItemClipsChildrenToShape)
        self.setFlag(self.ItemIsFocusable, True)  ## so we can receive key presses
        
        ## childGroup is required so that ViewBox has local coordinates similar to device coordinates.
129
        ## this is a workaround for a Qt + OpenGL bug that causes improper clipping
130
131
132
133
        ## https://bugreports.qt.nokia.com/browse/QTBUG-23723
        self.childGroup = ChildGroup(self)
        self.childGroup.sigItemsChanged.connect(self.itemsChanged)
        
134
135
136
137
138
139
        self.background = QtGui.QGraphicsRectItem(self.rect())
        self.background.setParentItem(self)
        self.background.setZValue(-1e6)
        self.background.setPen(fn.mkPen(None))
        self.updateBackground()
        
140
141
142
143
144
        #self.useLeftButtonPan = pyqtgraph.getConfigOption('leftButtonPan') # normally use left button to pan
        # this also enables capture of keyPressEvents.
        
        ## Make scale box that is shown when dragging on the view
        self.rbScaleBox = QtGui.QGraphicsRectItem(0, 0, 1, 1)
145
        self.rbScaleBox.setPen(fn.mkPen((255,255,100), width=1))
146
        self.rbScaleBox.setBrush(fn.mkBrush(255,255,0,100))
Luke Campagnola's avatar
Luke Campagnola committed
147
        self.rbScaleBox.setZValue(1e9)
148
        self.rbScaleBox.hide()
Luke Campagnola's avatar
Luke Campagnola committed
149
        self.addItem(self.rbScaleBox, ignoreBounds=True)
150
        
Luke Campagnola's avatar
Luke Campagnola committed
151
152
153
154
155
156
        ## show target rect for debugging
        self.target = QtGui.QGraphicsRectItem(0, 0, 1, 1)
        self.target.setPen(fn.mkPen('r'))
        self.target.setParentItem(self)
        self.target.hide()
        
157
158
159
160
161
162
163
164
        self.axHistory = [] # maintain a history of zoom locations
        self.axHistoryPointer = -1 # pointer into the history. Allows forward/backward movement, not just "undo"
        
        self.setZValue(-100)
        self.setSizePolicy(QtGui.QSizePolicy(QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Expanding))
        
        self.setAspectLocked(lockAspect)
        
Luke Campagnola's avatar
Luke Campagnola committed
165
        self.border = fn.mkPen(border)
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
        self.menu = ViewBoxMenu(self)
        
        self.register(name)
        if name is None:
            self.updateViewLists()
        
    def register(self, name):
        """
        Add this ViewBox to the registered list of views. 
        *name* will appear in the drop-down lists for axis linking in all other views.
        The same can be accomplished by initializing the ViewBox with the *name* attribute.
        """
        ViewBox.AllViews[self] = None
        if self.name is not None:
            del ViewBox.NamedViews[self.name]
        self.name = name
        if name is not None:
            ViewBox.NamedViews[name] = self
            ViewBox.updateAllViewLists()
Luke Campagnola's avatar
Luke Campagnola committed
185
            sid = id(self)
186
            self.destroyed.connect(lambda: ViewBox.forgetView(sid, name) if (ViewBox is not None and 'sid' in locals() and 'name' in locals()) else None)
187
            #self.destroyed.connect(self.unregister)
188
189

    def unregister(self):
Luke Campagnola's avatar
Luke Campagnola committed
190
        """
Luke Campagnola's avatar
Luke Campagnola committed
191
        Remove this ViewBox from the list of linkable views. (see :func:`register() <pyqtgraph.ViewBox.register>`)
Luke Campagnola's avatar
Luke Campagnola committed
192
        """
193
194
195
196
197
198
199
200
201
202
        del ViewBox.AllViews[self]
        if self.name is not None:
            del ViewBox.NamedViews[self.name]

    def close(self):
        self.unregister()

    def implements(self, interface):
        return interface == 'ViewBox'
        
203
    def itemChange(self, change, value):
204
205
        # Note: Calling QWidget.itemChange causes segv in python 3 + PyQt
        ret = QtGui.QGraphicsItem.itemChange(self, change, value)
206
207
208
209
210
211
212
213
214
215
216
        if change == self.ItemSceneChange:
            scene = self.scene()
            if scene is not None:
                scene.sigPrepareForPaint.disconnect(self.prepareForPaint)
        elif change == self.ItemSceneHasChanged:
            scene = self.scene()
            if scene is not None:
                scene.sigPrepareForPaint.connect(self.prepareForPaint)
        return ret

    def prepareForPaint(self):
Luke Campagnola's avatar
Fixes:    
Luke Campagnola committed
217
218
219
        #autoRangeEnabled = (self.state['autoRange'][0] is not False) or (self.state['autoRange'][1] is not False)
        # don't check whether auto range is enabled here--only check when setting dirty flag.
        if self._autoRangeNeedsUpdate: # and autoRangeEnabled: 
220
221
222
            self.updateAutoRange()
        if self._matrixNeedsUpdate:
            self.updateMatrix()
223
224
        
    def getState(self, copy=True):
225
226
        """Return the current state of the ViewBox. 
        Linked views are always converted to view names in the returned state."""
227
        state = self.state.copy()
228
229
230
231
232
233
234
235
236
        views = []
        for v in state['linkedViews']:
            if isinstance(v, weakref.ref):
                v = v()
            if v is None or isinstance(v, basestring):
                views.append(v)
            else:
                views.append(v.name)
        state['linkedViews'] = views
237
        if copy:
238
            return deepcopy(state)
239
        else:
240
            return state
241
242
        
    def setState(self, state):
243
244
        """Restore the state of this ViewBox.
        (see also getState)"""
245
246
247
248
249
250
        state = state.copy()
        self.setXLink(state['linkedViews'][0])
        self.setYLink(state['linkedViews'][1])
        del state['linkedViews']
        
        self.state.update(state)
251
252
        #self.updateMatrix()
        self.updateViewRange()
253
254
255
256
        self.sigStateChanged.emit(self)


    def setMouseMode(self, mode):
Luke Campagnola's avatar
Luke Campagnola committed
257
258
259
260
261
        """
        Set the mouse interaction mode. *mode* must be either ViewBox.PanMode or ViewBox.RectMode.
        In PanMode, the left mouse button pans the view and the right button scales.
        In RectMode, the left button draws a rectangle which updates the visible region (this mode is more suitable for single-button mice)
        """
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
        if mode not in [ViewBox.PanMode, ViewBox.RectMode]:
            raise Exception("Mode must be ViewBox.PanMode or ViewBox.RectMode")
        self.state['mouseMode'] = mode
        self.sigStateChanged.emit(self)

    #def toggleLeftAction(self, act):  ## for backward compatibility
        #if act.text() is 'pan':
            #self.setLeftButtonAction('pan')
        #elif act.text() is 'zoom':
            #self.setLeftButtonAction('rect')

    def setLeftButtonAction(self, mode='rect'):  ## for backward compatibility
        if mode.lower() == 'rect':
            self.setMouseMode(ViewBox.RectMode)
        elif mode.lower() == 'pan':
            self.setMouseMode(ViewBox.PanMode)
        else:
            raise Exception('graphicsItems:ViewBox:setLeftButtonAction: unknown mode = %s (Options are "pan" and "rect")' % mode)
            
    def innerSceneItem(self):
        return self.childGroup
    
    def setMouseEnabled(self, x=None, y=None):
Luke Campagnola's avatar
Luke Campagnola committed
285
286
287
288
        """
        Set whether each axis is enabled for mouse interaction. *x*, *y* arguments must be True or False.
        This allows the user to pan/scale one axis of the view while leaving the other axis unchanged.
        """
289
290
291
292
293
294
295
296
        if x is not None:
            self.state['mouseEnabled'][0] = x
        if y is not None:
            self.state['mouseEnabled'][1] = y
        self.sigStateChanged.emit(self)
            
    def mouseEnabled(self):
        return self.state['mouseEnabled'][:]
297
298
299
300
301
302
303
        
    def setMenuEnabled(self, enableMenu=True):
        self.state['enableMenu'] = enableMenu
        self.sigStateChanged.emit(self)

    def menuEnabled(self):
        return self.state.get('enableMenu', True)       
304
    
305
    def addItem(self, item, ignoreBounds=False):
Luke Campagnola's avatar
Luke Campagnola committed
306
307
308
309
        """
        Add a QGraphicsItem to this view. The view will include this item when determining how to set its range
        automatically unless *ignoreBounds* is True.
        """
310
311
        if item.zValue() < self.zValue():
            item.setZValue(self.zValue()+1)
Luke Campagnola's avatar
Luke Campagnola committed
312
313
314
        scene = self.scene()
        if scene is not None and scene is not item.scene():
            scene.addItem(item)  ## Necessary due to Qt bug: https://bugreports.qt-project.org/browse/QTBUG-18616
315
        item.setParentItem(self.childGroup)
316
317
        if not ignoreBounds:
            self.addedItems.append(item)
318
319
320
321
        self.updateAutoRange()
        #print "addItem:", item, item.boundingRect()
        
    def removeItem(self, item):
Luke Campagnola's avatar
Luke Campagnola committed
322
        """Remove an item from this view."""
Luke Campagnola's avatar
Luke Campagnola committed
323
324
325
326
        try:
            self.addedItems.remove(item)
        except:
            pass
327
328
329
        self.scene().removeItem(item)
        self.updateAutoRange()

Luke Campagnola's avatar
Luke Campagnola committed
330
331
332
333
    def clear(self):
        for i in self.addedItems[:]:
            self.removeItem(i)
        for ch in self.childGroup.childItems():
Luke Campagnola's avatar
Luke Campagnola committed
334
            ch.setParentItem(None)
Luke Campagnola's avatar
Luke Campagnola committed
335
        
336
    def resizeEvent(self, ev):
337
338
        self.linkedXChanged()
        self.linkedYChanged()
339
        self.updateAutoRange()
340
        self.updateViewRange()
341
        self.sigStateChanged.emit(self)
342
        self.background.setRect(self.rect())
Luke Campagnola's avatar
Luke Campagnola committed
343
        self.sigResized.emit(self)
344
        
Luke Campagnola's avatar
Fixes:    
Luke Campagnola committed
345
        
346
    def viewRange(self):
Luke Campagnola's avatar
Luke Campagnola committed
347
        """Return a the view's visible range as a list: [[xmin, xmax], [ymin, ymax]]"""
348
349
350
351
352
353
354
355
356
        return [x[:] for x in self.state['viewRange']]  ## return copy

    def viewRect(self):
        """Return a QRectF bounding the region visible within the ViewBox"""
        try:
            vr0 = self.state['viewRange'][0]
            vr1 = self.state['viewRange'][1]
            return QtCore.QRectF(vr0[0], vr1[0], vr0[1]-vr0[0], vr1[1] - vr1[0])
        except:
357
            print("make qrectf failed:", self.state['viewRange'])
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
            raise
    
    def targetRange(self):
        return [x[:] for x in self.state['targetRange']]  ## return copy
    
    def targetRect(self):  
        """
        Return the region which has been requested to be visible. 
        (this is not necessarily the same as the region that is *actually* visible--
        resizing and aspect ratio constraints can cause targetRect() and viewRect() to differ)
        """
        try:
            tr0 = self.state['targetRange'][0]
            tr1 = self.state['targetRange'][1]
            return QtCore.QRectF(tr0[0], tr1[0], tr0[1]-tr0[0], tr1[1] - tr1[0])
        except:
374
            print("make qrectf failed:", self.state['targetRange'])
375
376
            raise

Luke Campagnola's avatar
Luke Campagnola committed
377
    def setRange(self, rect=None, xRange=None, yRange=None, padding=None, update=True, disableAutoRange=True):
378
379
        """
        Set the visible range of the ViewBox.
Luke Campagnola's avatar
Luke Campagnola committed
380
        Must specify at least one of *rect*, *xRange*, or *yRange*. 
381
        
382
        ================== =====================================================================
Luke Campagnola's avatar
Luke Campagnola committed
383
        **Arguments**
384
385
386
387
388
389
390
391
392
393
394
        *rect*             (QRectF) The full range that should be visible in the view box.
        *xRange*           (min,max) The range that should be visible along the x-axis.
        *yRange*           (min,max) The range that should be visible along the y-axis.
        *padding*          (float) Expand the view by a fraction of the requested range. 
                           By default, this value is set between 0.02 and 0.1 depending on
                           the size of the ViewBox.
        *update*           (bool) If True, update the range of the ViewBox immediately. 
                           Otherwise, the update is deferred until before the next render.
        *disableAutoRange* (bool) If True, auto-ranging is diabled. Otherwise, it is left
                           unchanged.
        ================== =====================================================================
395
396
        
        """
397
        #print self.name, "ViewBox.setRange", rect, xRange, yRange, padding
Luke Campagnola's avatar
Fixes:    
Luke Campagnola committed
398
399
        #import traceback
        #traceback.print_stack()
400
        
401
402
        changes = {}   # axes
        setRequested = [False, False]
403
404
405
        
        if rect is not None:
            changes = {0: [rect.left(), rect.right()], 1: [rect.top(), rect.bottom()]}
406
            setRequested = [True, True]
407
408
        if xRange is not None:
            changes[0] = xRange
409
            setRequested[0] = True
410
411
        if yRange is not None:
            changes[1] = yRange
412
            setRequested[1] = True
413
414

        if len(changes) == 0:
415
            print(rect)
Luke Campagnola's avatar
Luke Campagnola committed
416
            raise Exception("Must specify at least one of rect, xRange, or yRange. (gave rect=%s)" % str(type(rect)))
417
        
418
        # Update axes one at a time
419
        changed = [False, False]
420
        for ax, range in changes.items():
421
422
            mn = min(range)
            mx = max(range)
423
424
425
426
            
            # If we requested 0 range, try to preserve previous scale. 
            # Otherwise just pick an arbitrary scale.
            if mn == mx:   
427
428
429
430
431
                dy = self.state['viewRange'][ax][1] - self.state['viewRange'][ax][0]
                if dy == 0:
                    dy = 1
                mn -= dy*0.5
                mx += dy*0.5
Luke Campagnola's avatar
Luke Campagnola committed
432
                xpad = 0.0
433
                
434
435
436
437
438
439
440
441
442
            # Make sure no nan/inf get through
            if not all(np.isfinite([mn, mx])):
                raise Exception("Cannot set range [%s, %s]" % (str(mn), str(mx)))
            
            # Apply padding
            if padding is None:
                xpad = self.suggestPadding(ax)
            else:
                xpad = padding
Luke Campagnola's avatar
Luke Campagnola committed
443
            p = (mx-mn) * xpad
444
445
            mn -= p
            mx += p
446
447
            
            # Set target range
448
449
450
            if self.state['targetRange'][ax] != [mn, mx]:
                self.state['targetRange'][ax] = [mn, mx]
                changed[ax] = True
Luke Campagnola's avatar
Luke Campagnola committed
451
                
452
453
454
455
456
457
458
459
        # Update viewRange to match targetRange as closely as possible while 
        # accounting for aspect ratio constraint
        lockX, lockY = setRequested
        if lockX and lockY:
            lockX = False
            lockY = False
        self.updateViewRange(lockX, lockY)
            
Luke Campagnola's avatar
Fixes:    
Luke Campagnola committed
460
        # Disable auto-range for each axis that was requested to be set
461
        if disableAutoRange:
Luke Campagnola's avatar
Fixes:    
Luke Campagnola committed
462
463
464
            xOff = False if setRequested[0] else None
            yOff = False if setRequested[1] else None
            self.enableAutoRange(x=xOff, y=yOff)
465
466
467
468
469
470
471
            changed.append(True)

        # If nothing has changed, we are done.
        if any(changed):
            #if update and self.matrixNeedsUpdate:
                #self.updateMatrix(changed)
            #return 
472
        
473
474
475
476
477
478
479
480
            self.sigStateChanged.emit(self)
            
            # Update target rect for debugging
            if self.target.isVisible():
                self.target.setRect(self.mapRectFromItem(self.childGroup, self.targetRect()))
                
        # If ortho axes have auto-visible-only, update them now
        # Note that aspect ratio constraints and auto-visible probably do not work together..
Luke Campagnola's avatar
Fixes:    
Luke Campagnola committed
481
        if changed[0] and self.state['autoVisibleOnly'][1] and (self.state['autoRange'][0] is not False):
482
483
            self._autoRangeNeedsUpdate = True
            #self.updateAutoRange()  ## Maybe just indicate that auto range needs to be updated?
Luke Campagnola's avatar
Fixes:    
Luke Campagnola committed
484
        elif changed[1] and self.state['autoVisibleOnly'][0] and (self.state['autoRange'][1] is not False):
485
486
            self._autoRangeNeedsUpdate = True
            #self.updateAutoRange()
487
            
488
489
490
491
492
493
        ## Update view matrix only if requested
        #if update:
            #self.updateMatrix(changed)
        ## Otherwise, indicate that the matrix needs to be updated
        #else:
            #self.matrixNeedsUpdate = True
494
            
495
496
497
498
499
500
        ## Inform linked views that the range has changed <<This should be moved>>
        #for ax, range in changes.items():
            #link = self.linkedView(ax)
            #if link is not None:
                #link.linkedViewChanged(self, ax)

501
502

            
Luke Campagnola's avatar
Luke Campagnola committed
503
    def setYRange(self, min, max, padding=None, update=True):
Luke Campagnola's avatar
Luke Campagnola committed
504
505
506
        """
        Set the visible Y range of the view to [*min*, *max*]. 
        The *padding* argument causes the range to be set larger by the fraction specified.
Luke Campagnola's avatar
Luke Campagnola committed
507
        (by default, this value is between 0.02 and 0.1 depending on the size of the ViewBox)
Luke Campagnola's avatar
Luke Campagnola committed
508
        """
509
510
        self.setRange(yRange=[min, max], update=update, padding=padding)
        
Luke Campagnola's avatar
Luke Campagnola committed
511
    def setXRange(self, min, max, padding=None, update=True):
Luke Campagnola's avatar
Luke Campagnola committed
512
513
514
        """
        Set the visible X range of the view to [*min*, *max*]. 
        The *padding* argument causes the range to be set larger by the fraction specified.
Luke Campagnola's avatar
Luke Campagnola committed
515
        (by default, this value is between 0.02 and 0.1 depending on the size of the ViewBox)
Luke Campagnola's avatar
Luke Campagnola committed
516
        """
517
518
        self.setRange(xRange=[min, max], update=update, padding=padding)

Luke Campagnola's avatar
Luke Campagnola committed
519
    def autoRange(self, padding=None, items=None, item=None):
520
521
        """
        Set the range of the view box to make all children visible.
Luke Campagnola's avatar
Luke Campagnola committed
522
523
        Note that this is not the same as enableAutoRange, which causes the view to 
        automatically auto-range whenever its contents are changed.
Luke Campagnola's avatar
Luke Campagnola committed
524
525
526
527
528
529
530
531
532
        
        =========== ============================================================
        Arguments
        padding     The fraction of the total data range to add on to the final
                    visible range. By default, this value is set between 0.02
                    and 0.1 depending on the size of the ViewBox.
        items       If specified, this is a list of items to consider when 
                    determining the visible range. 
        =========== ============================================================
533
        """
Luke Campagnola's avatar
Luke Campagnola committed
534
        if item is None:
Luke Campagnola's avatar
Luke Campagnola committed
535
            bounds = self.childrenBoundingRect(items=items)
Luke Campagnola's avatar
Luke Campagnola committed
536
        else:
537
            print("Warning: ViewBox.autoRange(item=__) is deprecated. Use 'items' argument instead.")
Luke Campagnola's avatar
Luke Campagnola committed
538
539
            bounds = self.mapFromItemToView(item, item.boundingRect()).boundingRect()
            
540
541
542
        if bounds is not None:
            self.setRange(bounds, padding=padding)
            
Luke Campagnola's avatar
Luke Campagnola committed
543
544
545
546
547
548
549
    def suggestPadding(self, axis):
        l = self.width() if axis==0 else self.height()
        if l > 0:
            padding = np.clip(1./(l**0.5), 0.02, 0.1)
        else:
            padding = 0.02
        return padding
550
            
551
    def scaleBy(self, s=None, center=None, x=None, y=None):
552
553
        """
        Scale by *s* around given center point (or center of view).
554
555
556
557
558
        *s* may be a Point or tuple (x, y).
        
        Optionally, x or y may be specified individually. This allows the other 
        axis to be left unaffected (note that using a scale factor of 1.0 may
        cause slight changes due to floating-point error).
559
        """
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
        if s is not None:
            scale = Point(s)
        else:
            scale = [x, y]
        
        affect = [True, True]
        if scale[0] is None and scale[1] is None:
            return
        elif scale[0] is None:
            affect[0] = False
            scale[0] = 1.0
        elif scale[1] is None:
            affect[1] = False
            scale[1] = 1.0
            
        scale = Point(scale)
            
577
        if self.state['aspectLocked'] is not False:
Luke Campagnola's avatar
Luke Campagnola committed
578
            scale[0] = scale[1]
579
580
581
582
583
584

        vr = self.targetRect()
        if center is None:
            center = Point(vr.center())
        else:
            center = Point(center)
585
        
586
587
588
        tl = center + (vr.topLeft()-center) * scale
        br = center + (vr.bottomRight()-center) * scale
        
589
590
591
592
593
594
595
596
        if not affect[0]:
            self.setYRange(tl.y(), br.y(), padding=0)
        elif not affect[1]:
            self.setXRange(tl.x(), br.x(), padding=0)
        else:
            self.setRange(QtCore.QRectF(tl, br), padding=0)
        
    def translateBy(self, t=None, x=None, y=None):
597
598
599
        """
        Translate the view by *t*, which may be a Point or tuple (x, y).
        
600
601
602
603
        Alternately, x or y may be specified independently, leaving the other
        axis unchanged (note that using a translation of 0 may still cause
        small changes due to floating-point error).
        """
604
        vr = self.targetRect()
605
606
607
        if t is not None:
            t = Point(t)
            self.setRange(vr.translated(t), padding=0)
608
609
        else:
            if x is not None:
610
                x = vr.left()+x, vr.right()+x
611
            if y is not None:
612
613
                y = vr.top()+y, vr.bottom()+y
            self.setRange(xRange=x, yRange=y, padding=0)
614
615
            
        
616
        
617
    def enableAutoRange(self, axis=None, enable=True, x=None, y=None):
618
        """
Luke Campagnola's avatar
Luke Campagnola committed
619
620
        Enable (or disable) auto-range for *axis*, which may be ViewBox.XAxis, ViewBox.YAxis, or ViewBox.XYAxes for both
        (if *axis* is omitted, both axes will be changed).
621
622
623
624
625
626
627
628
        When enabled, the axis will automatically rescale when items are added/removed or change their shape.
        The argument *enable* may optionally be a float (0.0-1.0) which indicates the fraction of the data that should
        be visible (this only works with items implementing a dataRange method, such as PlotDataItem).
        """
        #print "autorange:", axis, enable
        #if not enable:
            #import traceback
            #traceback.print_stack()
629
630
631
632
633
634
635
636
637
        
        # support simpler interface:
        if x is not None or y is not None:
            if x is not None:
                self.enableAutoRange(ViewBox.XAxis, x)
            if y is not None:
                self.enableAutoRange(ViewBox.YAxis, y)
            return
        
638
639
640
641
642
643
        if enable is True:
            enable = 1.0
        
        if axis is None:
            axis = ViewBox.XYAxes
        
644
645
        needAutoRangeUpdate = False
        
646
        if axis == ViewBox.XYAxes or axis == 'xy':
647
            axes = [0, 1]
648
        elif axis == ViewBox.XAxis or axis == 'x':
649
            axes = [0]
650
        elif axis == ViewBox.YAxis or axis == 'y':
651
            axes = [1]
652
653
654
        else:
            raise Exception('axis argument must be ViewBox.XAxis, ViewBox.YAxis, or ViewBox.XYAxes.')
        
655
656
        for ax in axes:
            if self.state['autoRange'][ax] != enable:
Luke Campagnola's avatar
Fixes:    
Luke Campagnola committed
657
658
659
660
661
                # If we are disabling, do one last auto-range to make sure that
                # previously scheduled auto-range changes are enacted
                if enable is False and self._autoRangeNeedsUpdate:
                    self.updateAutoRange()
                
662
                self.state['autoRange'][ax] = enable
Luke Campagnola's avatar
Fixes:    
Luke Campagnola committed
663
664
665
666
667
668
                self._autoRangeNeedsUpdate |= (enable is not False)
                self.update()


        #if needAutoRangeUpdate:
        #    self.updateAutoRange()
669
        
670
671
672
        self.sigStateChanged.emit(self)

    def disableAutoRange(self, axis=None):
Luke Campagnola's avatar
Luke Campagnola committed
673
        """Disables auto-range. (See enableAutoRange)"""
674
675
676
677
678
        self.enableAutoRange(axis, enable=False)

    def autoRangeEnabled(self):
        return self.state['autoRange'][:]

679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
    def setAutoPan(self, x=None, y=None):
        if x is not None:
            self.state['autoPan'][0] = x
        if y is not None:
            self.state['autoPan'][1] = y
        if None not in [x,y]:
            self.updateAutoRange()

    def setAutoVisible(self, x=None, y=None):
        if x is not None:
            self.state['autoVisibleOnly'][0] = x
            if x is True:
                self.state['autoVisibleOnly'][1] = False
        if y is not None:
            self.state['autoVisibleOnly'][1] = y
            if y is True:
                self.state['autoVisibleOnly'][0] = False
        
        if x is not None or y is not None:
            self.updateAutoRange()

700
    def updateAutoRange(self):
701
702
703
704
        ## Break recursive loops when auto-ranging.
        ## This is needed because some items change their size in response 
        ## to a view change.
        if self._updatingRange:
705
            return
706
707
708
709
710
711
712
713
714
715
716
        
        self._updatingRange = True
        try:
            targetRect = self.viewRange()
            if not any(self.state['autoRange']):
                return
                
            fractionVisible = self.state['autoRange'][:]
            for i in [0,1]:
                if type(fractionVisible[i]) is bool:
                    fractionVisible[i] = 1.0
717

718
            childRange = None
Luke Campagnola's avatar
Luke Campagnola committed
719
            
720
721
722
            order = [0,1]
            if self.state['autoVisibleOnly'][0] is True:
                order = [1,0]
723

724
725
726
727
728
729
730
731
732
            args = {}
            for ax in order:
                if self.state['autoRange'][ax] is False:
                    continue
                if self.state['autoVisibleOnly'][ax]:
                    oRange = [None, None]
                    oRange[ax] = targetRect[1-ax]
                    childRange = self.childrenBounds(frac=fractionVisible, orthoRange=oRange)
                    
733
                else:
734
735
736
737
738
739
740
741
742
743
744
                    if childRange is None:
                        childRange = self.childrenBounds(frac=fractionVisible)
                
                ## Make corrections to range
                xr = childRange[ax]
                if xr is not None:
                    if self.state['autoPan'][ax]:
                        x = sum(xr) * 0.5
                        w2 = (targetRect[ax][1]-targetRect[ax][0]) / 2.
                        childRange[ax] = [x-w2, x+w2]
                    else:
Luke Campagnola's avatar
Luke Campagnola committed
745
746
747
748
                        padding = self.suggestPadding(ax)
                        wp = (xr[1] - xr[0]) * padding
                        childRange[ax][0] -= wp
                        childRange[ax][1] += wp
749
750
751
752
753
754
755
756
                    targetRect[ax] = childRange[ax]
                    args['xRange' if ax == 0 else 'yRange'] = targetRect[ax]
            if len(args) == 0:
                return
            args['padding'] = 0
            args['disableAutoRange'] = False
            self.setRange(**args)
        finally:
757
            self._autoRangeNeedsUpdate = False
758
            self._updatingRange = False
759
760
        
    def setXLink(self, view):
Luke Campagnola's avatar
Luke Campagnola committed
761
        """Link this view's X axis to another view. (see LinkView)"""
762
763
764
        self.linkView(self.XAxis, view)
        
    def setYLink(self, view):
Luke Campagnola's avatar
Luke Campagnola committed
765
        """Link this view's Y axis to another view. (see LinkView)"""
766
767
768
769
770
771
772
773
774
775
776
777
        self.linkView(self.YAxis, view)
        
        
    def linkView(self, axis, view):
        """
        Link X or Y axes of two views and unlink any previously connected axes. *axis* must be ViewBox.XAxis or ViewBox.YAxis.
        If view is None, the axis is left unlinked.
        """
        if isinstance(view, basestring):
            if view == '':
                view = None
            else:
778
                view = ViewBox.NamedViews.get(view, view)  ## convert view name to ViewBox if possible
779
780
781
782
783
784
785
786
787
788
789
790
791

        if hasattr(view, 'implements') and view.implements('ViewBoxWrapper'):
            view = view.getViewBox()

        ## used to connect/disconnect signals between a pair of views
        if axis == ViewBox.XAxis:
            signal = 'sigXRangeChanged'
            slot = self.linkedXChanged
        else:
            signal = 'sigYRangeChanged'
            slot = self.linkedYChanged


792
        oldLink = self.linkedView(axis)
793
        if oldLink is not None:
794
795
            try:
                getattr(oldLink, signal).disconnect(slot)
796
                oldLink.sigResized.disconnect(slot)
797
798
799
            except TypeError:
                ## This can occur if the view has been deleted already
                pass
800
801
            
        
802
803
804
805
        if view is None or isinstance(view, basestring):
            self.state['linkedViews'][axis] = view
        else:
            self.state['linkedViews'][axis] = weakref.ref(view)
806
            getattr(view, signal).connect(slot)
807
            view.sigResized.connect(slot)
808
            if view.autoRangeEnabled()[axis] is not False:
809
810
811
812
813
                self.enableAutoRange(axis, False)
                slot()
            else:
                if self.autoRangeEnabled()[axis] is False:
                    slot()
Luke Campagnola's avatar
Luke Campagnola committed
814
        
815
816
817
818
819
820
821
            
        self.sigStateChanged.emit(self)
        
    def blockLink(self, b):
        self.linksBlocked = b  ## prevents recursive plot-change propagation

    def linkedXChanged(self):
822
        ## called when x range of linked view has changed
823
        view = self.linkedView(0)
824
825
826
        self.linkedViewChanged(view, ViewBox.XAxis)

    def linkedYChanged(self):
827
        ## called when y range of linked view has changed
828
        view = self.linkedView(1)
829
830
        self.linkedViewChanged(view, ViewBox.YAxis)
        
831
832
833
834
835
836
837
838
    def linkedView(self, ax):
        ## Return the linked view for axis *ax*.
        ## this method _always_ returns either a ViewBox or None.
        v = self.state['linkedViews'][ax]
        if v is None or isinstance(v, basestring):
            return None
        else:
            return v()  ## dereference weakref pointer. If the reference is dead, this returns None
839
840

    def linkedViewChanged(self, view, axis):
841
        if self.linksBlocked or view is None:
842
843
            return
        
844
        #print self.name, "ViewBox.linkedViewChanged", axis, view.viewRange()[axis]
845
846
847
        vr = view.viewRect()
        vg = view.screenGeometry()
        sg = self.screenGeometry()
848
849
        if vg is None or sg is None:
            return
850
851
852
853
        
        view.blockLink(True)
        try:
            if axis == ViewBox.XAxis:
854
855
856
857
858
859
860
861
862
                overlap = min(sg.right(), vg.right()) - max(sg.left(), vg.left())
                if overlap < min(vg.width()/3, sg.width()/3):  ## if less than 1/3 of views overlap, 
                                                               ## then just replicate the view
                    x1 = vr.left()
                    x2 = vr.right()
                else:  ## views overlap; line them up
                    upp = float(vr.width()) / vg.width()
                    x1 = vr.left() + (sg.x()-vg.x()) * upp
                    x2 = x1 + sg.width() * upp
863
864
865
                self.enableAutoRange(ViewBox.XAxis, False)
                self.setXRange(x1, x2, padding=0)
            else:
866
867
                overlap = min(sg.bottom(), vg.bottom()) - max(sg.top(), vg.top())
                if overlap < min(vg.height()/3, sg.height()/3):  ## if less than 1/3 of views overlap, 
Luke Campagnola's avatar
Luke Campagnola committed
868
                                                                 ## then just replicate the view
869
870
                    y1 = vr.top()
                    y2 = vr.bottom()
871
872
                else:  ## views overlap; line them up
                    upp = float(vr.height()) / vg.height()
Luke Campagnola's avatar
Luke Campagnola committed
873
874
875
876
                    if self.yInverted():
                        y2 = vr.bottom() + (sg.bottom()-vg.bottom()) * upp
                    else:
                        y2 = vr.bottom() + (sg.top()-vg.top()) * upp
877
                    y1 = y2 - sg.height() * upp
878
                self.enableAutoRange(ViewBox.YAxis, False)
879
                self.setYRange(y1, y2, padding=0)
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
        finally:
            view.blockLink(False)
        
        
    def screenGeometry(self):
        """return the screen geometry of the viewbox"""
        v = self.getViewWidget()
        if v is None:
            return None
        b = self.sceneBoundingRect()
        wr = v.mapFromScene(b).boundingRect()
        pos = v.mapToGlobal(v.pos())
        wr.adjust(pos.x(), pos.y(), pos.x(), pos.y())
        return wr
        
    

    def itemsChanged(self):
        ## called when items are added/removed from self.childGroup
        self.updateAutoRange()
        
    def itemBoundsChanged(self, item):
Luke Campagnola's avatar
Luke Campagnola committed
902
        self._itemBoundsCache.pop(item, None)
Luke Campagnola's avatar
Fixes:    
Luke Campagnola committed
903
904
905
        if (self.state['autoRange'][0] is not False) or (self.state['autoRange'][1] is not False):
            self._autoRangeNeedsUpdate = True
            self.update()
906
        #self.updateAutoRange()
907
908
909
910
911
912

    def invertY(self, b=True):
        """
        By default, the positive y-axis points upward on the screen. Use invertY(True) to reverse the y-axis.
        """
        self.state['yInverted'] = b
913
914
        #self.updateMatrix(changed=(False, True))
        self.updateViewRange()
915
        self.sigStateChanged.emit(self)
916
917
918

    def yInverted(self):
        return self.state['yInverted']
919
920
921
922
923
        
    def setAspectLocked(self, lock=True, ratio=1):
        """
        If the aspect ratio is locked, view scaling must always preserve the aspect ratio.
        By default, the ratio is set to 1; x and y both have the same scaling.
Luke Campagnola's avatar
Luke Campagnola committed
924
        This ratio can be overridden (xScale/yScale), or use None to lock in the current ratio.
925
926
927
928
        """
        if not lock:
            self.state['aspectLocked'] = False
        else:
Luke Campagnola's avatar
Luke Campagnola committed
929
            rect = self.rect()
930
            vr = self.viewRect()
Luke Campagnola's avatar
Luke Campagnola committed
931
932
933
934
            if rect.height() == 0 or vr.width() == 0 or vr.height() == 0:
                currentRatio = 1.0
            else:
                currentRatio = (rect.width()/float(rect.height())) / (vr.width()/vr.height())
935
936
937
938
939
            if ratio is None:
                ratio = currentRatio
            self.state['aspectLocked'] = ratio
            if ratio != currentRatio:  ## If this would change the current range, do that now
                #self.setRange(0, self.state['viewRange'][0][0], self.state['viewRange'][0][1])
940
                self.updateViewRange()
941
942
943
944
945
946
947
948
949
950
951
952
953
954
        self.sigStateChanged.emit(self)
        
    def childTransform(self):
        """
        Return the transform that maps from child(item in the childGroup) coordinates to local coordinates.
        (This maps from inside the viewbox to outside)
        """ 
        m = self.childGroup.transform()
        #m1 = QtGui.QTransform()
        #m1.translate(self.childGroup.pos().x(), self.childGroup.pos().y())
        return m #*m1

    def mapToView(self, obj):
        """Maps from the local coordinates of the ViewBox to the coordinate system displayed inside the ViewBox"""
Luke Campagnola's avatar
Luke Campagnola committed
955
        m = fn.invertQTransform(self.childTransform())
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
        return m.map(obj)

    def mapFromView(self, obj):
        """Maps from the coordinate system displayed inside the ViewBox to the local coordinates of the ViewBox"""
        m = self.childTransform()
        return m.map(obj)

    def mapSceneToView(self, obj):
        """Maps from scene coordinates to the coordinate system displayed inside the ViewBox"""
        return self.mapToView(self.mapFromScene(obj))

    def mapViewToScene(self, obj):
        """Maps from the coordinate system displayed inside the ViewBox to scene coordinates"""
        return self.mapToScene(self.mapFromView(obj))
    
    def mapFromItemToView(self, item, obj):
Luke Campagnola's avatar
Luke Campagnola committed
972
        """Maps *obj* from the local coordinate system of *item* to the view coordinates"""
Luke Campagnola's avatar
Luke Campagnola committed
973
974
        return self.childGroup.mapFromItem(item, obj)
        #return self.mapSceneToView(item.mapToScene(obj))
975
976

    def mapFromViewToItem(self, item, obj):
Luke Campagnola's avatar
Luke Campagnola committed
977
        """Maps *obj* from view coordinates to the local coordinate system of *item*."""
Luke Campagnola's avatar
Luke Campagnola committed
978
979
        return self.childGroup.mapToItem(item, obj)
        #return item.mapFromScene(self.mapViewToScene(obj))
980

Luke Campagnola's avatar
Luke Campagnola committed
981
982
983
984
985
986
987
988
989
990
991
992
993
    def mapViewToDevice(self, obj):
        return self.mapToDevice(self.mapFromView(obj))
        
    def mapDeviceToView(self, obj):
        return self.mapToView(self.mapFromDevice(obj))
        
    def viewPixelSize(self):
        """Return the (width, height) of a screen pixel in view coordinates."""
        o = self.mapToView(Point(0,0))
        px, py = [Point(self.mapToView(v) - o) for v in self.pixelVectors()]
        return (px.length(), py.length())
        
        
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
    def itemBoundingRect(self, item):
        """Return the bounding rect of the item in view coordinates"""
        return self.mapSceneToView(item.sceneBoundingRect()).boundingRect()
    
    #def viewScale(self):
        #vr = self.viewRect()
        ##print "viewScale:", self.range
        #xd = vr.width()
        #yd = vr.height()
        #if xd == 0 or yd == 0:
            #print "Warning: 0 range in view:", xd, yd
            #return np.array([1,1])
        
        ##cs = self.canvas().size()
        #cs = self.boundingRect()
        #scale = np.array([cs.width() / xd, cs.height() / yd])
        ##print "view scale:", scale
        #return scale

    def wheelEvent(self, ev, axis=None):
        mask = np.array(self.state['mouseEnabled'], dtype=np.float)
        if axis is not None and axis >= 0 and axis < len(mask):
            mv = mask[axis]
            mask[:] = 0
            mask[axis] = mv
        s = ((mask * 0.02) + 1) ** (ev.delta() * self.state['wheelScaleFactor']) # actual scaling factor
        
Luke Campagnola's avatar
Luke Campagnola committed
1021
        center = Point(fn.invertQTransform(self.childGroup.transform()).map(ev.pos()))
1022
1023
1024
1025
1026
1027
1028
1029
        #center = ev.pos()
        
        self.scaleBy(s, center)
        self.sigRangeChangedManually.emit(self.state['mouseEnabled'])
        ev.accept()

        
    def mouseClickEvent(self, ev):
1030
        if ev.button() == QtCore.Qt.RightButton and self.menuEnabled():
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
            ev.accept()
            self.raiseContextMenu(ev)
    
    def raiseContextMenu(self, ev):
        #print "viewbox.raiseContextMenu called."
        
        #menu = self.getMenu(ev)
        menu = self.getMenu(ev)
        self.scene().addParentContextMenus(self, menu, ev)
        #print "2:", [str(a.text()) for a in self.menu.actions()]
        pos = ev.screenPos()
        #pos2 = ev.scenePos()
        #print "3:", [str(a.text()) for a in self.menu.actions()]
        #self.sigActionPositionChanged.emit(pos2)

        menu.popup(QtCore.QPoint(pos.x(), pos.y()))
        #print "4:", [str(a.text()) for a in self.menu.actions()]
        
    def getMenu(self, ev):
        self._menuCopy = self.menu.copy()  ## temporary storage to prevent menu disappearing
        return self._menuCopy
        
    def getContextMenus(self, event):
1054
1055
1056
1057
        if self.menuEnabled():
            return self.menu.subMenus()
        else:
            return None
1058
1059
1060
        #return [self.getMenu(event)]
        

Luke Campagnola's avatar
Luke Campagnola committed
1061
1062
    def mouseDragEvent(self, ev, axis=None):
        ## if axis is specified, event will only affect that axis.
1063
1064
1065
1066
1067
1068
1069
1070
        ev.accept()  ## we accept all buttons
        
        pos = ev.pos()
        lastPos = ev.lastPos()
        dif = pos - lastPos
        dif = dif * -1

        ## Ignore axes if mouse is disabled
1071
1072
        mouseEnabled = np.array(self.state['mouseEnabled'], dtype=np.float)
        mask = mouseEnabled.copy()
Luke Campagnola's avatar
Luke Campagnola committed
1073
1074
        if axis is not None:
            mask[1-axis] = 0.0
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093

        ## Scale or translate based on mouse button
        if ev.button() & (QtCore.Qt.LeftButton | QtCore.Qt.MidButton):
            if self.state['mouseMode'] == ViewBox.RectMode:
                if ev.isFinish():  ## This is the final move in the drag; change the view scale now
                    #print "finish"
                    self.rbScaleBox.hide()
                    #ax = QtCore.QRectF(Point(self.pressPos), Point(self.mousePos))
                    ax = QtCore.QRectF(Point(ev.buttonDownPos(ev.button())), Point(pos))
                    ax = self.childGroup.mapRectFromParent(ax)
                    self.showAxRect(ax)
                    self.axHistoryPointer += 1
                    self.axHistory = self.axHistory[:self.axHistoryPointer] + [ax]
                else:
                    ## update shape of scale box
                    self.updateScaleBox(ev.buttonDownPos(), ev.pos())
            else:
                tr = dif*mask
                tr = self.mapToView(tr) - self.mapToView(Point(0,0))
1094
1095
1096
1097
                x = tr.x() if mask[0] == 1 else None
                y = tr.y() if mask[1] == 1 else None
                
                self.translateBy(x=x, y=y)
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
                self.sigRangeChangedManually.emit(self.state['mouseEnabled'])
        elif ev.button() & QtCore.Qt.RightButton:
            #print "vb.rightDrag"
            if self.state['aspectLocked'] is not False:
                mask[0] = 0
            
            dif = ev.screenPos() - ev.lastScreenPos()
            dif = np.array([dif.x(), dif.y()])
            dif[0] *= -1
            s = ((mask * 0.02) + 1) ** dif
Luke Campagnola's avatar
Luke Campagnola committed
1108
1109
1110
1111
            
            tr = self.childGroup.transform()
            tr = fn.invertQTransform(tr)
            
1112
1113
            x = s[0] if mouseEnabled[0] == 1 else None
            y = s[1] if mouseEnabled[1] == 1 else None
1114
            
Luke Campagnola's avatar
Luke Campagnola committed
1115
            center = Point(tr.map(ev.buttonDownPos(QtCore.Qt.RightButton)))
1116
            self.scaleBy(x=x, y=y, center=center)
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
            self.sigRangeChangedManually.emit(self.state['mouseEnabled'])

    def keyPressEvent(self, ev):
        """
        This routine should capture key presses in the current view box.
        Key presses are used only when mouse mode is RectMode
        The following events are implemented:
        ctrl-A : zooms out to the default "full" view of the plot
        ctrl-+ : moves forward in the zooming stack (if it exists)
        ctrl-- : moves backward in the zooming stack (if it exists)
         
        """
        #print ev.key()
        #print 'I intercepted a key press, but did not accept it'
        
        ## not implemented yet ?
        #self.keypress.sigkeyPressEvent.emit()
        
        ev.accept()
        if ev.text() == '-':
            self.scaleHistory(-1)
        elif ev.text() in ['+', '=']:
            self.scaleHistory(1)
        elif ev.key() == QtCore.Qt.Key_Backspace:
            self.scaleHistory(len(self.axHistory))
        else:
            ev.ignore()

    def scaleHistory(self, d):
        ptr = max(0, min(len(self.axHistory)-1, self.axHistoryPointer+d))
        if ptr != self.axHistoryPointer:
            self.axHistoryPointer = ptr
            self.showAxRect(self.axHistory[ptr])
            

    def updateScaleBox(self, p1, p2):
        r = QtCore.QRectF(p1, p2)
        r = self.childGroup.mapRectFromParent(r)
        self.rbScaleBox.setPos(r.topLeft())
        self.rbScaleBox.resetTransform()
        self.rbScaleBox.scale(r.width(), r.height())
        self.rbScaleBox.show()

    def showAxRect(self, ax):
        self.setRange(ax.normalized()) # be sure w, h are correct coordinates
        self.sigRangeChangedManually.emit(self.state['mouseEnabled'])

    #def mouseRect(self):
        #vs = self.viewScale()
        #vr = self.state['viewRange']
        ## Convert positions from screen (view) pixel coordinates to axis coordinates 
        #ax = QtCore.QRectF(self.pressPos[0]/vs[0]+vr[0][0], -(self.pressPos[1]/vs[1]-vr[1][1]),
            #(self.mousePos[0]-self.pressPos[0])/vs[0], -(self.mousePos[1]-self.pressPos[1])/vs[1])
        #return(ax)

    def allChildren(self, item=None):
        """Return a list of all children and grandchildren of this ViewBox"""
        if item is None:
            item = self.childGroup
        
        children = [item]
        for ch in item.childItems():
            children.extend(self.allChildren(ch))
        return children
        
        
1183
    
Luke Campagnola's avatar
Luke Campagnola committed
1184
    def childrenBounds(self, frac=None, orthoRange=(None,None), items=None):
1185
1186
1187
1188
        """Return the bounding range of all children.
        [[xmin, xmax], [ymin, ymax]]
        Values may be None if there are no specific bounds for an axis.
        """
Luke Campagnola's avatar
Luke Campagnola committed
1189
        prof = debug.Profiler('updateAutoRange', disabled=True)
Luke Campagnola's avatar
Luke Campagnola committed
1190
1191
        if items is None:
            items = self.addedItems
1192
        
1193
1194
1195
1196
1197
        ## measure pixel dimensions in view box
        px, py = [v.length() if v is not None else 0 for v in self.childGroup.pixelVectors()]
        
        ## First collect all boundary information
        itemBounds = []
1198
1199
1200
1201
1202
1203
        for item in items:
            if not item.isVisible():
                continue
        
            useX = True
            useY = True
1204
            
1205
            if hasattr(item, 'dataBounds'):
1206
1207
1208
1209
1210
1211
1212
                #bounds = self._itemBoundsCache.get(item, None)
                #if bounds is None:
                if frac is None:
                    frac = (1.0, 1.0)
                xr = item.dataBounds(0, frac=frac[0], orthoRange=orthoRange[0])
                yr = item.dataBounds(1, frac=frac[1], orthoRange=orthoRange[1])
                pxPad = 0 if not hasattr(item, 'pixelPadding') else item.pixelPadding()
Luke Campagnola's avatar
Luke Campagnola committed
1213
                if xr is None or (xr[0] is None and xr[1] is None) or np.isnan(xr).any() or np.isinf(xr).any():
1214
1215
                    useX = False
                    xr = (0,0)
Luke Campagnola's avatar
Luke Campagnola committed
1216
                if yr is None or (yr[0] is None and yr[1] is None) or np.isnan(yr).any() or np.isinf(yr).any():
1217
1218
                    useY = False
                    yr = (0,0)
Luke Campagnola's avatar
Luke Campagnola committed
1219

1220
1221
1222
1223
1224
1225
1226
1227
1228
1229
1230
1231
1232
1233
1234
1235
1236
1237
1238
1239
1240
1241
1242
                bounds = QtCore.QRectF(xr[0], yr[0], xr[1]-xr[0], yr[1]-yr[0])
                bounds = self.mapFromItemToView(item, bounds).boundingRect()
                
                if not any([useX, useY]):
                    continue
                
                ## If we are ignoring only one axis, we need to check for rotations
                if useX != useY:  ##   !=  means  xor
                    ang = round(item.transformAngle())
                    if ang == 0 or ang == 180:
                        pass
                    elif ang == 90 or ang == 270:
                        useX, useY = useY, useX 
                    else:
                        ## Item is rotated at non-orthogonal angle, ignore bounds entirely.
                        ## Not really sure what is the expected behavior in this case.
                        continue  ## need to check for item rotations and decide how best to apply this boundary. 
                
                
                itemBounds.append((bounds, useX, useY, pxPad))
                    #self._itemBoundsCache[item] = (bounds, useX, useY)
                #else:
                    #bounds, useX, useY = bounds
1243
1244
1245
1246
1247
            else:
                if int(item.flags() & item.ItemHasNoContents) > 0:
                    continue
                else:
                    bounds = item.boundingRect()
Luke Campagnola's avatar
Luke Campagnola committed
1248
                bounds = self.mapFromItemToView(item, bounds).boundingRect()
1249
1250
1251
1252
1253
1254
1255
                itemBounds.append((bounds, True, True, 0))
        
        #print itemBounds
        
        ## determine tentative new range
        range = [None, None]
        for bounds, useX, useY, px in itemBounds:
1256
1257
1258
1259
1260
1261
1262
1263
1264
1265
            if useY:
                if range[1] is not None:
                    range[1] = [min(bounds.top(), range[1][0]), max(bounds.bottom(), range[1][1])]
                else:
                    range[1] = [bounds.top(), bounds.bottom()]
            if useX:
                if range[0] is not None:
                    range[0] = [min(bounds.left(), range[0][0]), max(bounds.right(), range[0][1])]
                else:
                    range[0] = [bounds.left(), bounds.right()]
Luke Campagnola's avatar
Luke Campagnola committed
1266
            prof.mark('2')
1267
1268
1269
1270
1271
1272
1273
1274
1275
1276
1277
1278
1279
1280
1281
1282
1283
1284
1285
1286
1287
1288
1289
1290
1291
1292
        
        #print "range", range
        
        ## Now expand any bounds that have a pixel margin
        ## This must be done _after_ we have a good estimate of the new range
        ## to ensure that the pixel size is roughly accurate.
        w = self.width()
        h = self.height()
        #print "w:", w, "h:", h
        if w > 0 and range[0] is not None:
            pxSize = (range[0][1] - range[0][0]) / w
            for bounds, useX, useY, px in itemBounds:
                if px == 0 or not useX:
                    continue
                range[0][0] = min(range[0][0], bounds.left() - px*pxSize)
                range[0][1] = max(range[0][1], bounds.right() + px*pxSize)
        if h > 0 and range[1] is not None:
            pxSize = (range[1][1] - range[1][0]) / h
            for bounds, useX, useY, px in itemBounds:
                if px == 0 or not useY:
                    continue
                range[1][0] = min(range[1][0], bounds.top() - px*pxSize)
                range[1][1] = max(range[1][1], bounds.bottom() + px*pxSize)
        
        #print "final range", range
        
Luke Campagnola's avatar
Luke Campagnola committed
1293
        prof.finish()