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

__all__ = ['ViewBox']

17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35
class WeakList(object):

    def __init__(self):
        self._items = []

    def append(self, obj):
        #Add backwards to iterate backwards (to make iterating more efficient on removal).
        self._items.insert(0, weakref.ref(obj))

    def __iter__(self):
        i = len(self._items)-1
        while i >= 0:
            ref = self._items[i]
            d = ref()
            if d is None:
                del self._items[i]
            else:
                yield d
            i -= 1
36 37 38

class ChildGroup(ItemGroup):
    
39 40
    def __init__(self, parent):
        ItemGroup.__init__(self, parent)
Luke Campagnola's avatar
Luke Campagnola committed
41 42 43 44 45 46 47
        
        # Used as callback to inform ViewBox when items are added/removed from 
        # the group. 
        # Note 1: We would prefer to override itemChange directly on the 
        #         ViewBox, but this causes crashes on PySide.
        # Note 2: We might also like to use a signal rather than this callback
        #         mechanism, but this causes a different PySide crash.        
48 49
        self.itemsChangedListeners = WeakList()
 
50 51
        # excempt from telling view when transform changes
        self._GraphicsObject__inform_view_on_change = False
52 53 54 55
    
    def itemChange(self, change, value):
        ret = ItemGroup.itemChange(self, change, value)
        if change == self.ItemChildAddedChange or change == self.ItemChildRemovedChange:
56 57 58 59 60 61 62 63 64
            try:
                itemsChangedListeners = self.itemsChangedListeners
            except AttributeError:
                # It's possible that the attribute was already collected when the itemChange happened
                # (if it was triggered during the gc of the object).
                pass
            else:
                for listener in itemsChangedListeners:
                    listener.itemsChanged()
65 66 67 68 69
        return ret


class ViewBox(GraphicsWidget):
    """
Luke Campagnola's avatar
Luke Campagnola committed
70 71
    **Bases:** :class:`GraphicsWidget <pyqtgraph.GraphicsWidget>`
    
72
    Box that allows internal scaling/panning of children by mouse drag. 
Luke Campagnola's avatar
Luke Campagnola committed
73 74 75 76
    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:
    
77 78 79 80
    * 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
Luke Campagnola's avatar
Luke Campagnola committed
81
    
82 83 84 85 86 87 88 89
    """
    
    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)
90
    sigTransformChanged = QtCore.Signal(object)
Luke Campagnola's avatar
Luke Campagnola committed
91
    sigResized = QtCore.Signal(object)
92 93 94 95 96 97 98 99 100 101 102 103 104 105
    
    ## 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
    
106
    def __init__(self, parent=None, border=None, lockAspect=False, enableMouse=True, invertY=False, enableMenu=True, name=None, invertX=False):
Luke Campagnola's avatar
Luke Campagnola committed
107
        """
tommy3001's avatar
tommy3001 committed
108
        ==============  =============================================================
109
        **Arguments:**
tommy3001's avatar
tommy3001 committed
110 111 112 113 114 115 116
        *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>`
117
        *invertX*       (bool) See :func:`invertX <pyqtgraph.ViewBox.invertX>`
118 119 120 121 122 123
        *enableMenu*    (bool) Whether to display a context menu when 
                        right-clicking on the ViewBox background.
        *name*          (str) Used to register this ViewBox so that it appears
                        in the "Link axis" dropdown inside other ViewBox
                        context menus. This allows the user to manually link
                        the axes of any other view to this one. 
tommy3001's avatar
tommy3001 committed
124
        ==============  =============================================================
Luke Campagnola's avatar
Luke Campagnola committed
125 126
        """
        
127 128 129 130 131 132
        GraphicsWidget.__init__(self, parent)
        self.name = None
        self.linksBlocked = False
        self.addedItems = []
        #self.gView = view
        #self.showGrid = showGrid
133 134
        self._matrixNeedsUpdate = True  ## indicates that range has changed, but matrix update was deferred
        self._autoRangeNeedsUpdate = True ## indicates auto-range needs to be recomputed.
135 136

        self._lastScene = None  ## stores reference to the last known scene this view was a part of.
137 138 139 140 141 142 143 144 145
        
        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,
146
            'xInverted': invertX,
147 148 149
            '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
150 151
            '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 
152 153
            '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.
154 155
            
            'mouseEnabled': [enableMouse, enableMouse],
156
            'mouseMode': ViewBox.PanMode if getConfigOption('leftButtonPan') else ViewBox.RectMode,  
157
            'enableMenu': enableMenu,
158
            'wheelScaleFactor': -1.0 / 8.0,
159 160

            'background': None,
161 162 163
            
            # Limits
            'limits': {
164 165 166 167
                'xLimits': [None, None],   # Maximum and minimum visible X values 
                'yLimits': [None, None],   # Maximum and minimum visible Y values  
                'xRange': [None, None],   # Maximum and minimum X range
                'yRange': [None, None],   # Maximum and minimum Y range 
168 169
                }
            
170
        }
171
        self._updatingRange = False  ## Used to break recursive loops. See updateAutoRange.
Luke Campagnola's avatar
Luke Campagnola committed
172
        self._itemBoundsCache = weakref.WeakKeyDictionary()
173
        
Luke Campagnola's avatar
Luke Campagnola committed
174
        self.locateGroup = None  ## items displayed when using ViewBox.locate(item)
175 176 177 178 179
        
        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.
180
        ## this is a workaround for a Qt + OpenGL bug that causes improper clipping
181 182
        ## https://bugreports.qt.nokia.com/browse/QTBUG-23723
        self.childGroup = ChildGroup(self)
183
        self.childGroup.itemsChangedListeners.append(self)
184
        
185 186 187 188 189 190
        self.background = QtGui.QGraphicsRectItem(self.rect())
        self.background.setParentItem(self)
        self.background.setZValue(-1e6)
        self.background.setPen(fn.mkPen(None))
        self.updateBackground()
        
191 192 193 194 195
        #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)
196
        self.rbScaleBox.setPen(fn.mkPen((255,255,100), width=1))
197
        self.rbScaleBox.setBrush(fn.mkBrush(255,255,0,100))
Luke Campagnola's avatar
Luke Campagnola committed
198
        self.rbScaleBox.setZValue(1e9)
199
        self.rbScaleBox.hide()
Luke Campagnola's avatar
Luke Campagnola committed
200
        self.addItem(self.rbScaleBox, ignoreBounds=True)
201
        
Luke Campagnola's avatar
Luke Campagnola committed
202 203 204 205 206 207
        ## 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()
        
208 209 210 211 212 213 214 215
        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
216
        self.border = fn.mkPen(border)
217 218 219 220 221 222 223 224 225
        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. 
226 227 228 229 230
        
        This allows users to manually link the axes of any other ViewBox to
        this one. The specified *name* will appear in the drop-down lists for 
        axis linking in the context menus of all other views.
        
231 232 233 234 235 236 237 238 239
        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
240
            sid = id(self)
241
            self.destroyed.connect(lambda: ViewBox.forgetView(sid, name) if (ViewBox is not None and 'sid' in locals() and 'name' in locals()) else None)
242
            #self.destroyed.connect(self.unregister)
243 244

    def unregister(self):
Luke Campagnola's avatar
Luke Campagnola committed
245
        """
Luke Campagnola's avatar
Luke Campagnola committed
246
        Remove this ViewBox from the list of linkable views. (see :func:`register() <pyqtgraph.ViewBox.register>`)
Luke Campagnola's avatar
Luke Campagnola committed
247
        """
248 249 250 251 252
        del ViewBox.AllViews[self]
        if self.name is not None:
            del ViewBox.NamedViews[self.name]

    def close(self):
253
        self.clear()
254 255 256 257 258
        self.unregister()

    def implements(self, interface):
        return interface == 'ViewBox'
        
259 260 261 262 263 264 265 266 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
    # removed due to https://bugreports.qt-project.org/browse/PYSIDE-86
    #def itemChange(self, change, value):
        ## Note: Calling QWidget.itemChange causes segv in python 3 + PyQt
        ##ret = QtGui.QGraphicsItem.itemChange(self, change, value)
        #ret = GraphicsWidget.itemChange(self, change, value)
        #if change == self.ItemSceneChange:
            #scene = self.scene()
            #if scene is not None and hasattr(scene, 'sigPrepareForPaint'):
                #scene.sigPrepareForPaint.disconnect(self.prepareForPaint)
        #elif change == self.ItemSceneHasChanged:
            #scene = self.scene()
            #if scene is not None and hasattr(scene, 'sigPrepareForPaint'):
                #scene.sigPrepareForPaint.connect(self.prepareForPaint)
        #return ret
        
    def checkSceneChange(self):
        # ViewBox needs to receive sigPrepareForPaint from its scene before 
        # being painted. However, we have no way of being informed when the
        # scene has changed in order to make this connection. The usual way
        # to do this is via itemChange(), but bugs prevent this approach
        # (see above). Instead, we simply check at every paint to see whether
        # (the scene has changed.
        scene = self.scene()
        if scene == self._lastScene:
            return
        if self._lastScene is not None and hasattr(self.lastScene, 'sigPrepareForPaint'):
            self._lastScene.sigPrepareForPaint.disconnect(self.prepareForPaint)
        if scene is not None and hasattr(scene, 'sigPrepareForPaint'):
            scene.sigPrepareForPaint.connect(self.prepareForPaint)
        self.prepareForPaint()
        self._lastScene = scene
            
            
        
293 294

    def prepareForPaint(self):
Luke Campagnola's avatar
Fixes:  
Luke Campagnola committed
295 296 297
        #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: 
298 299 300
            self.updateAutoRange()
        if self._matrixNeedsUpdate:
            self.updateMatrix()
301 302
        
    def getState(self, copy=True):
303 304
        """Return the current state of the ViewBox. 
        Linked views are always converted to view names in the returned state."""
305
        state = self.state.copy()
306 307 308 309 310 311 312 313 314
        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
315
        if copy:
316
            return deepcopy(state)
317
        else:
318
            return state
319 320
        
    def setState(self, state):
321 322
        """Restore the state of this ViewBox.
        (see also getState)"""
323 324 325 326 327 328
        state = state.copy()
        self.setXLink(state['linkedViews'][0])
        self.setYLink(state['linkedViews'][1])
        del state['linkedViews']
        
        self.state.update(state)
329 330
        #self.updateMatrix()
        self.updateViewRange()
331 332
        self.sigStateChanged.emit(self)

333 334 335 336 337
    def setBackgroundColor(self, color):
        """
        Set the background color of the ViewBox.
        
        If color is None, then no background will be drawn.
Luke Campagnola's avatar
Luke Campagnola committed
338 339
        
        Added in version 0.9.9
340 341 342 343
        """
        self.background.setVisible(color is not None)
        self.state['background'] = color
        self.updateBackground()
344 345

    def setMouseMode(self, mode):
Luke Campagnola's avatar
Luke Campagnola committed
346 347 348 349 350
        """
        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)
        """
351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373
        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
374 375 376 377
        """
        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.
        """
378 379 380 381 382 383 384 385
        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'][:]
386 387 388 389 390 391 392
        
    def setMenuEnabled(self, enableMenu=True):
        self.state['enableMenu'] = enableMenu
        self.sigStateChanged.emit(self)

    def menuEnabled(self):
        return self.state.get('enableMenu', True)       
393
    
394
    def addItem(self, item, ignoreBounds=False):
Luke Campagnola's avatar
Luke Campagnola committed
395 396 397 398
        """
        Add a QGraphicsItem to this view. The view will include this item when determining how to set its range
        automatically unless *ignoreBounds* is True.
        """
399 400
        if item.zValue() < self.zValue():
            item.setZValue(self.zValue()+1)
Luke Campagnola's avatar
Luke Campagnola committed
401 402 403
        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
404
        item.setParentItem(self.childGroup)
405 406
        if not ignoreBounds:
            self.addedItems.append(item)
407 408 409 410
        self.updateAutoRange()
        #print "addItem:", item, item.boundingRect()
        
    def removeItem(self, item):
Luke Campagnola's avatar
Luke Campagnola committed
411
        """Remove an item from this view."""
Luke Campagnola's avatar
Luke Campagnola committed
412 413 414 415
        try:
            self.addedItems.remove(item)
        except:
            pass
416 417 418
        self.scene().removeItem(item)
        self.updateAutoRange()

Luke Campagnola's avatar
Luke Campagnola committed
419 420 421 422
    def clear(self):
        for i in self.addedItems[:]:
            self.removeItem(i)
        for ch in self.childGroup.childItems():
Luke Campagnola's avatar
Luke Campagnola committed
423
            ch.setParentItem(None)
Luke Campagnola's avatar
Luke Campagnola committed
424
        
425
    def resizeEvent(self, ev):
426 427
        self.linkedXChanged()
        self.linkedYChanged()
428
        self.updateAutoRange()
429
        self.updateViewRange()
Luke Campagnola's avatar
Luke Campagnola committed
430
        self._matrixNeedsUpdate = True
431
        self.sigStateChanged.emit(self)
432
        self.background.setRect(self.rect())
Luke Campagnola's avatar
Luke Campagnola committed
433
        self.sigResized.emit(self)
434 435
        
    def viewRange(self):
Luke Campagnola's avatar
Luke Campagnola committed
436
        """Return a the view's visible range as a list: [[xmin, xmax], [ymin, ymax]]"""
437 438 439 440 441 442 443 444 445
        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:
446
            print("make qrectf failed:", self.state['viewRange'])
447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462
            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:
463
            print("make qrectf failed:", self.state['targetRange'])
464 465
            raise

466 467 468 469 470 471 472
    def _resetTarget(self):
        # Reset target range to exactly match current view range.
        # This is used during mouse interaction to prevent unpredictable
        # behavior (because the user is unaware of targetRange).
        if self.state['aspectLocked'] is False: # (interferes with aspect locking)
            self.state['targetRange'] = [self.state['viewRange'][0][:], self.state['viewRange'][1][:]]

Luke Campagnola's avatar
Luke Campagnola committed
473
    def setRange(self, rect=None, xRange=None, yRange=None, padding=None, update=True, disableAutoRange=True):
474 475
        """
        Set the visible range of the ViewBox.
Luke Campagnola's avatar
Luke Campagnola committed
476
        Must specify at least one of *rect*, *xRange*, or *yRange*. 
477
        
478
        ================== =====================================================================
479
        **Arguments:**
480 481 482 483 484 485 486 487 488 489 490
        *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.
        ================== =====================================================================
491 492
        
        """
493
        #print self.name, "ViewBox.setRange", rect, xRange, yRange, padding
Luke Campagnola's avatar
Fixes:  
Luke Campagnola committed
494 495
        #import traceback
        #traceback.print_stack()
496
        
497 498
        changes = {}   # axes
        setRequested = [False, False]
499 500 501
        
        if rect is not None:
            changes = {0: [rect.left(), rect.right()], 1: [rect.top(), rect.bottom()]}
502
            setRequested = [True, True]
503 504
        if xRange is not None:
            changes[0] = xRange
505
            setRequested[0] = True
506 507
        if yRange is not None:
            changes[1] = yRange
508
            setRequested[1] = True
509 510

        if len(changes) == 0:
511
            print(rect)
Luke Campagnola's avatar
Luke Campagnola committed
512
            raise Exception("Must specify at least one of rect, xRange, or yRange. (gave rect=%s)" % str(type(rect)))
513
        
514
        # Update axes one at a time
515
        changed = [False, False]
516
        for ax, range in changes.items():
517 518
            mn = min(range)
            mx = max(range)
519 520 521 522
            
            # If we requested 0 range, try to preserve previous scale. 
            # Otherwise just pick an arbitrary scale.
            if mn == mx:   
523 524 525 526 527
                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
528
                xpad = 0.0
529
                
530 531 532 533 534 535 536 537 538
            # 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
539
            p = (mx-mn) * xpad
540 541
            mn -= p
            mx += p
542 543
            
            # Set target range
544 545 546
            if self.state['targetRange'][ax] != [mn, mx]:
                self.state['targetRange'][ax] = [mn, mx]
                changed[ax] = True
Luke Campagnola's avatar
Luke Campagnola committed
547
                
548 549 550 551 552 553 554 555
        # 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
556
        # Disable auto-range for each axis that was requested to be set
557
        if disableAutoRange:
Luke Campagnola's avatar
Fixes:  
Luke Campagnola committed
558 559 560
            xOff = False if setRequested[0] else None
            yOff = False if setRequested[1] else None
            self.enableAutoRange(x=xOff, y=yOff)
561 562 563 564 565 566 567
            changed.append(True)

        # If nothing has changed, we are done.
        if any(changed):
            #if update and self.matrixNeedsUpdate:
                #self.updateMatrix(changed)
            #return 
568
        
569 570 571 572 573 574 575 576
            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
577
        if changed[0] and self.state['autoVisibleOnly'][1] and (self.state['autoRange'][0] is not False):
578 579
            self._autoRangeNeedsUpdate = True
            #self.updateAutoRange()  ## Maybe just indicate that auto range needs to be updated?
Luke Campagnola's avatar
Fixes:  
Luke Campagnola committed
580
        elif changed[1] and self.state['autoVisibleOnly'][0] and (self.state['autoRange'][1] is not False):
581 582
            self._autoRangeNeedsUpdate = True
            #self.updateAutoRange()
583
            
584 585 586 587 588 589
        ## Update view matrix only if requested
        #if update:
            #self.updateMatrix(changed)
        ## Otherwise, indicate that the matrix needs to be updated
        #else:
            #self.matrixNeedsUpdate = True
590
            
591 592 593 594 595 596
        ## 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)

597 598

            
Luke Campagnola's avatar
Luke Campagnola committed
599
    def setYRange(self, min, max, padding=None, update=True):
Luke Campagnola's avatar
Luke Campagnola committed
600 601 602
        """
        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
603
        (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
604
        """
605 606
        self.setRange(yRange=[min, max], update=update, padding=padding)
        
Luke Campagnola's avatar
Luke Campagnola committed
607
    def setXRange(self, min, max, padding=None, update=True):
Luke Campagnola's avatar
Luke Campagnola committed
608 609 610
        """
        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
611
        (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
612
        """
613 614
        self.setRange(xRange=[min, max], update=update, padding=padding)

Luke Campagnola's avatar
Luke Campagnola committed
615
    def autoRange(self, padding=None, items=None, item=None):
616 617
        """
        Set the range of the view box to make all children visible.
Luke Campagnola's avatar
Luke Campagnola committed
618 619
        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
620
        
tommy3001's avatar
tommy3001 committed
621 622 623 624 625 626 627 628
        ==============  ============================================================
        **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.
        ==============  ============================================================
629
        """
Luke Campagnola's avatar
Luke Campagnola committed
630
        if item is None:
Luke Campagnola's avatar
Luke Campagnola committed
631
            bounds = self.childrenBoundingRect(items=items)
Luke Campagnola's avatar
Luke Campagnola committed
632
        else:
633
            print("Warning: ViewBox.autoRange(item=__) is deprecated. Use 'items' argument instead.")
Luke Campagnola's avatar
Luke Campagnola committed
634 635
            bounds = self.mapFromItemToView(item, item.boundingRect()).boundingRect()
            
636 637 638
        if bounds is not None:
            self.setRange(bounds, padding=padding)
            
Luke Campagnola's avatar
Luke Campagnola committed
639 640 641 642 643 644 645
    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
646 647 648 649 650
    
    def setLimits(self, **kwds):
        """
        Set limits that constrain the possible view ranges.
        
651 652
        **Panning limits**. The following arguments define the region within the 
        viewbox coordinate system that may be accessed by panning the view.
Luke Campagnola's avatar
Luke Campagnola committed
653
        
654
        =========== ============================================================
655 656 657 658 659 660 661 662
        xMin        Minimum allowed x-axis value
        xMax        Maximum allowed x-axis value
        yMin        Minimum allowed y-axis value
        yMax        Maximum allowed y-axis value
        =========== ============================================================        
        
        **Scaling limits**. These arguments prevent the view being zoomed in or
        out too far.
Luke Campagnola's avatar
Luke Campagnola committed
663
        
664
        =========== ============================================================
665 666 667 668
        minXRange   Minimum allowed left-to-right span across the view.
        maxXRange   Maximum allowed left-to-right span across the view.
        minYRange   Minimum allowed top-to-bottom span across the view.
        maxYRange   Maximum allowed top-to-bottom span across the view.
Luke Campagnola's avatar
Luke Campagnola committed
669 670 671
        =========== ============================================================
        
        Added in version 0.9.9
672
        """
673
        update = False
674 675 676 677
        allowed = ['xMin', 'xMax', 'yMin', 'yMax', 'minXRange', 'maxXRange', 'minYRange', 'maxYRange']
        for kwd in kwds:
            if kwd not in allowed:
                raise ValueError("Invalid keyword argument '%s'." % kwd)
678 679 680 681
        #for kwd in ['xLimits', 'yLimits', 'minRange', 'maxRange']:
            #if kwd in kwds and self.state['limits'][kwd] != kwds[kwd]:
                #self.state['limits'][kwd] = kwds[kwd]
                #update = True
682 683 684
        for axis in [0,1]:
            for mnmx in [0,1]:
                kwd = [['xMin', 'xMax'], ['yMin', 'yMax']][axis][mnmx]
685 686 687 688 689
                lname = ['xLimits', 'yLimits'][axis]
                if kwd in kwds and self.state['limits'][lname][mnmx] != kwds[kwd]:
                    self.state['limits'][lname][mnmx] = kwds[kwd]
                    update = True
                kwd = [['minXRange', 'maxXRange'], ['minYRange', 'maxYRange']][axis][mnmx]
690 691 692 693 694 695 696 697 698 699
                lname = ['xRange', 'yRange'][axis]
                if kwd in kwds and self.state['limits'][lname][mnmx] != kwds[kwd]:
                    self.state['limits'][lname][mnmx] = kwds[kwd]
                    update = True
                    
        if update:
            self.updateViewRange()
                    
            
            
700
            
701
    def scaleBy(self, s=None, center=None, x=None, y=None):
702 703
        """
        Scale by *s* around given center point (or center of view).
704 705 706 707 708
        *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).
709
        """
710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726
        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)
            
727
        if self.state['aspectLocked'] is not False:
Luke Campagnola's avatar
Luke Campagnola committed
728
            scale[0] = scale[1]
729 730 731 732 733 734

        vr = self.targetRect()
        if center is None:
            center = Point(vr.center())
        else:
            center = Point(center)
735
        
736 737 738
        tl = center + (vr.topLeft()-center) * scale
        br = center + (vr.bottomRight()-center) * scale
        
739 740 741 742 743 744 745 746
        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):
747 748 749
        """
        Translate the view by *t*, which may be a Point or tuple (x, y).
        
750 751 752 753
        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).
        """
754
        vr = self.targetRect()
755 756 757
        if t is not None:
            t = Point(t)
            self.setRange(vr.translated(t), padding=0)
758 759
        else:
            if x is not None:
760
                x = vr.left()+x, vr.right()+x
761
            if y is not None:
762
                y = vr.top()+y, vr.bottom()+y
763 764
            if x is not None or y is not None:
                self.setRange(xRange=x, yRange=y, padding=0)
765 766
            
        
767
        
768
    def enableAutoRange(self, axis=None, enable=True, x=None, y=None):
769
        """
Luke Campagnola's avatar
Luke Campagnola committed
770 771
        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).
772 773 774 775 776 777 778 779
        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()
780 781 782 783 784 785 786 787 788
        
        # 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
        
789 790 791 792 793 794
        if enable is True:
            enable = 1.0
        
        if axis is None:
            axis = ViewBox.XYAxes
        
795 796
        needAutoRangeUpdate = False
        
797
        if axis == ViewBox.XYAxes or axis == 'xy':
798
            axes = [0, 1]
799
        elif axis == ViewBox.XAxis or axis == 'x':
800
            axes = [0]
801
        elif axis == ViewBox.YAxis or axis == 'y':
802
            axes = [1]
803 804 805
        else:
            raise Exception('axis argument must be ViewBox.XAxis, ViewBox.YAxis, or ViewBox.XYAxes.')
        
806 807
        for ax in axes:
            if self.state['autoRange'][ax] != enable:
Luke Campagnola's avatar
Fixes:  
Luke Campagnola committed
808 809 810 811 812
                # 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()
                
813
                self.state['autoRange'][ax] = enable
Luke Campagnola's avatar
Fixes:  
Luke Campagnola committed
814 815 816 817 818 819
                self._autoRangeNeedsUpdate |= (enable is not False)
                self.update()


        #if needAutoRangeUpdate:
        #    self.updateAutoRange()
820
        
821 822 823
        self.sigStateChanged.emit(self)

    def disableAutoRange(self, axis=None):
Luke Campagnola's avatar
Luke Campagnola committed
824
        """Disables auto-range. (See enableAutoRange)"""
825 826 827 828 829
        self.enableAutoRange(axis, enable=False)

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

830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850
    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()

851
    def updateAutoRange(self):
852 853 854 855
        ## Break recursive loops when auto-ranging.
        ## This is needed because some items change their size in response 
        ## to a view change.
        if self._updatingRange:
856
            return
857 858 859 860 861 862 863 864 865 866 867
        
        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
868

869
            childRange = None
Luke Campagnola's avatar
Luke Campagnola committed
870
            
871 872 873
            order = [0,1]
            if self.state['autoVisibleOnly'][0] is True:
                order = [1,0]
874

875 876 877 878 879 880 881 882 883
            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)
                    
884
                else:
885 886 887 888 889 890 891 892 893 894 895
                    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
896 897 898 899
                        padding = self.suggestPadding(ax)
                        wp = (xr[1] - xr[0]) * padding
                        childRange[ax][0] -= wp
                        childRange[ax][1] += wp
900 901 902 903 904 905
                    targetRect[ax] = childRange[ax]
                    args['xRange' if ax == 0 else 'yRange'] = targetRect[ax]
            if len(args) == 0:
                return
            args['padding'] = 0
            args['disableAutoRange'] = False
906 907 908 909 910 911
            
             # check for and ignore bad ranges
            for k in ['xRange', 'yRange']:
                if k in args:
                    if not np.all(np.isfinite(args[k])):
                        r = args.pop(k)
Luke Campagnola's avatar
Luke Campagnola committed
912
                        #print("Warning: %s is invalid: %s" % (k, str(r))
913
                        
914 915
            self.setRange(**args)
        finally:
916
            self._autoRangeNeedsUpdate = False
917
            self._updatingRange = False
918 919
        
    def setXLink(self, view):
Luke Campagnola's avatar
Luke Campagnola committed
920
        """Link this view's X axis to another view. (see LinkView)"""
921 922 923
        self.linkView(self.XAxis, view)
        
    def setYLink(self, view):
Luke Campagnola's avatar
Luke Campagnola committed
924
        """Link this view's Y axis to another view. (see LinkView)"""
925 926 927 928 929 930 931 932 933 934 935 936
        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:
937
                view = ViewBox.NamedViews.get(view, view)  ## convert view name to ViewBox if possible
938 939 940 941 942 943 944 945 946 947 948 949 950

        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


951
        oldLink = self.linkedView(axis)
952
        if oldLink is not None:
953 954
            try:
                getattr(oldLink, signal).disconnect(slot)
955
                oldLink.sigResized.disconnect(slot)
Luke Campagnola's avatar
Luke Campagnola committed
956
            except (TypeError, RuntimeError):
957 958
                ## This can occur if the view has been deleted already
                pass
959 960
            
        
961 962 963 964
        if view is None or isinstance(view, basestring):
            self.state['linkedViews'][axis] = view
        else:
            self.state['linkedViews'][axis] = weakref.ref(view)
965
            getattr(view, signal).connect(slot)
966
            view.sigResized.connect(slot)
967
            if view.autoRangeEnabled()[axis] is not False:
968 969 970 971 972
                self.enableAutoRange(axis, False)
                slot()
            else:
                if self.autoRangeEnabled()[axis] is False:
                    slot()
Luke Campagnola's avatar
Luke Campagnola committed
973
        
974 975 976 977 978 979 980
            
        self.sigStateChanged.emit(self)
        
    def blockLink(self, b):
        self.linksBlocked = b  ## prevents recursive plot-change propagation

    def linkedXChanged(self):
981
        ## called when x range of linked view has changed
982
        view = self.linkedView(0)
983 984 985
        self.linkedViewChanged(view, ViewBox.XAxis)

    def linkedYChanged(self):
986
        ## called when y range of linked view has changed
987
        view = self.linkedView(1)
988 989
        self.linkedViewChanged(view, ViewBox.YAxis)
        
990 991 992 993 994 995 996 997
    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
998 999

    def linkedViewChanged(self, view, axis):