From 662b319d7b707597b89e11b1cfee99e8b062f649 Mon Sep 17 00:00:00 2001 From: Luke Campagnola <> Date: Mon, 9 Jul 2012 08:38:30 -0400 Subject: [PATCH] - PlotItem can now be constructed with customized ViewBox and AxisItems - Text spacing fix for AxisItem --- GraphicsScene/GraphicsScene.py | 7 +- __init__.py | 12 +- examples/__main__.py | 3 +- examples/customPlot.py | 52 +++ graphicsItems/AxisItem.py | 28 +- graphicsItems/PlotItem/PlotItem.py | 583 ++++------------------------- graphicsItems/ViewBox/ViewBox.py | 11 +- widgets/PlotWidget.py | 2 + 8 files changed, 168 insertions(+), 530 deletions(-) create mode 100644 examples/customPlot.py diff --git a/GraphicsScene/GraphicsScene.py b/GraphicsScene/GraphicsScene.py index 73867059..d4dd8534 100644 --- a/GraphicsScene/GraphicsScene.py +++ b/GraphicsScene/GraphicsScene.py @@ -505,18 +505,19 @@ class GraphicsScene(QtGui.QGraphicsScene): menusToAdd = [] while item is not self: item = item.parentItem() - + if item is None: item = self if not hasattr(item, "getContextMenus"): continue - subMenus = item.getContextMenus(event) + if subMenus is None: + continue if type(subMenus) is not list: ## so that some items (like FlowchartViewBox) can return multiple menus subMenus = [subMenus] - + for sm in subMenus: menusToAdd.append(sm) diff --git a/__init__.py b/__init__.py index 756e46e8..b31f15d0 100644 --- a/__init__.py +++ b/__init__.py @@ -49,10 +49,10 @@ def getConfigOption(opt): def systemInfo(): - print "sys.platform:", sys.platform - print "sys.version:", sys.version + print("sys.platform: %s" % sys.platform) + print("sys.version: %s" % sys.version) from .Qt import VERSION_INFO - print "qt bindings:", VERSION_INFO + print("qt bindings: %s" % VERSION_INFO) global REVISION if REVISION is None: ## this code was probably checked out from bzr; look up the last-revision file @@ -60,8 +60,8 @@ def systemInfo(): if os.path.exists(lastRevFile): REVISION = open(lastRevFile, 'r').read().strip() - print "pyqtgraph:", REVISION - print "config:" + print("pyqtgraph: %s" % REVISION) + print("config:") import pprint pprint.pprint(CONFIG_OPTIONS) @@ -126,7 +126,7 @@ def importAll(path, excludes=()): globals()[k] = getattr(mod, k) importAll('graphicsItems') -importAll('widgets', excludes=['MatplotlibWidget']) +importAll('widgets', excludes=['MatplotlibWidget', 'RemoteGraphicsView']) from .imageview import * from .WidgetGroup import * diff --git a/examples/__main__.py b/examples/__main__.py index 0881fcea..39c51b7f 100644 --- a/examples/__main__.py +++ b/examples/__main__.py @@ -16,6 +16,7 @@ examples = OrderedDict([ ('Video speed test', 'VideoSpeedTest.py'), ('Plot speed test', 'PlotSpeedTest.py'), ('Data Slicing', 'DataSlicing.py'), + ('Plot Customization', 'customPlot.py'), ('GraphicsItems', OrderedDict([ ('Scatter Plot', 'ScatterPlot.py'), #('PlotItem', 'PlotItem.py'), @@ -45,7 +46,7 @@ examples = OrderedDict([ #('VerticalLabel', '../widgets/VerticalLabel.py'), ('JoystickButton', 'JoystickButton.py'), ])), - + ('GraphicsScene', 'GraphicsScene.py'), ('Flowcharts', 'Flowchart.py'), #('Canvas', '../canvas'), diff --git a/examples/customPlot.py b/examples/customPlot.py new file mode 100644 index 00000000..86ed4829 --- /dev/null +++ b/examples/customPlot.py @@ -0,0 +1,52 @@ +# -*- coding: utf-8 -*- +## +## This example demonstrates the creation of a plot with a customized +## AxisItem and ViewBox. +## + + +import initExample ## Add path to library (just for examples; you do not need this) + +import pyqtgraph as pg +from pyqtgraph.Qt import QtCore, QtGui +import numpy as np +import time + +class DateAxis(pg.AxisItem): + def tickStrings(self, values, scale, spacing): + return [time.strftime('%b %Y', time.localtime(x)) for x in values] + +class CustomViewBox(pg.ViewBox): + def __init__(self, *args, **kwds): + pg.ViewBox.__init__(self, *args, **kwds) + self.setMouseMode(self.RectMode) + + ## reimplement right-click to zoom out + def mouseClickEvent(self, ev): + if ev.button() == QtCore.Qt.RightButton: + self.autoRange() + + def mouseDragEvent(self, ev): + if ev.button() == QtCore.Qt.RightButton: + ev.ignore() + else: + pg.ViewBox.mouseDragEvent(self, ev) + + +app = pg.mkQApp() + +axis = DateAxis(orientation='bottom') +vb = CustomViewBox() + +pw = pg.PlotWidget(viewBox=vb, axisItems={'bottom': axis}, enableMenu=False, title="PlotItem with custom axis and ViewBox
Menu disabled, mouse behavior changed: left-drag to zoom, right-click to reset zoom") +dates = np.arange(8) * (3600*24*356) +pw.plot(x=dates, y=[1,6,2,4,3,5,6,8], symbol='o') +pw.show() + +r = pg.PolyLineROI([(0,0), (10, 10)]) +pw.addItem(r) + +## Start Qt event loop unless running in interactive mode or using pyside. +import sys +if (sys.flags.interactive != 1) or not hasattr(QtCore, 'PYQT_VERSION'): + QtGui.QApplication.instance().exec_() diff --git a/graphicsItems/AxisItem.py b/graphicsItems/AxisItem.py index 351b4ed5..bcf6ce5c 100644 --- a/graphicsItems/AxisItem.py +++ b/graphicsItems/AxisItem.py @@ -363,6 +363,29 @@ class AxisItem(GraphicsWidget): (intervals[minorIndex], 0) ] + ##### This does not work -- switching between 2/5 confuses the automatic text-level-selection + ### Determine major/minor tick spacings which flank the optimal spacing. + #intervals = np.array([1., 2., 5., 10., 20., 50., 100.]) * p10unit + #minorIndex = 0 + #while intervals[minorIndex+1] <= optimalSpacing: + #minorIndex += 1 + + ### make sure we never see 5 and 2 at the same time + #intIndexes = [ + #[0,1,3], + #[0,2,3], + #[2,3,4], + #[3,4,6], + #[3,5,6], + #][minorIndex] + + #return [ + #(intervals[intIndexes[2]], 0), + #(intervals[intIndexes[1]], 0), + #(intervals[intIndexes[0]], 0) + #] + + def tickValues(self, minVal, maxVal, size): """ @@ -395,7 +418,7 @@ class AxisItem(GraphicsWidget): ## remove any ticks that were present in higher levels ## we assume here that if the difference between a tick value and a previously seen tick value ## is less than spacing/100, then they are 'equal' and we can ignore the new tick. - values = filter(lambda x: all(np.abs(allValues-x) > spacing*0.01), values) + values = list(filter(lambda x: all(np.abs(allValues-x) > spacing*0.01), values) ) allValues = np.concatenate([allValues, values]) ticks.append((spacing, values)) @@ -601,9 +624,9 @@ class AxisItem(GraphicsWidget): if tickPositions[i][j] is None: strings[j] = None + textRects.extend([p.boundingRect(QtCore.QRectF(0, 0, 100, 100), QtCore.Qt.AlignCenter, s) for s in strings if s is not None]) if i > 0: ## always draw top level ## measure all text, make sure there's enough room - textRects.extend([p.boundingRect(QtCore.QRectF(0, 0, 100, 100), QtCore.Qt.AlignCenter, s) for s in strings if s is not None]) if axis == 0: textSize = np.sum([r.height() for r in textRects]) else: @@ -613,7 +636,6 @@ class AxisItem(GraphicsWidget): textFillRatio = float(textSize) / lengthInPixels if textFillRatio > 0.7: break - #spacing, values = tickLevels[best] #strings = self.tickStrings(values, self.scale, spacing) for j in range(len(strings)): diff --git a/graphicsItems/PlotItem/PlotItem.py b/graphicsItems/PlotItem/PlotItem.py index f027a434..dfd0ad65 100644 --- a/graphicsItems/PlotItem/PlotItem.py +++ b/graphicsItems/PlotItem/PlotItem.py @@ -33,17 +33,12 @@ from .. AxisItem import AxisItem from .. LabelItem import LabelItem from .. GraphicsWidget import GraphicsWidget from .. ButtonItem import ButtonItem +#from .. GraphicsLayout import GraphicsLayout from pyqtgraph.WidgetGroup import WidgetGroup import collections __all__ = ['PlotItem'] -#try: - #from WidgetGroup import * - #HAVE_WIDGETGROUP = True -#except: - #HAVE_WIDGETGROUP = False - try: from metaarray import * HAVE_METAARRAY = True @@ -99,26 +94,28 @@ class PlotItem(GraphicsWidget): lastFileDir = None managers = {} - def __init__(self, parent=None, name=None, labels=None, title=None, **kargs): + def __init__(self, parent=None, name=None, labels=None, title=None, viewBox=None, axisItems=None, enableMenu=True, **kargs): """ Create a new PlotItem. All arguments are optional. Any extra keyword arguments are passed to PlotItem.plot(). - ============= ========================================================================================== + ============== ========================================================================================== **Arguments** - *title* Title to display at the top of the item. Html is allowed. - *labels* A dictionary specifying the axis labels to display:: + *title* Title to display at the top of the item. Html is allowed. + *labels* A dictionary specifying the axis labels to display:: - {'left': (args), 'bottom': (args), ...} + {'left': (args), 'bottom': (args), ...} - The name of each axis and the corresponding arguments are passed to - :func:`PlotItem.setLabel() ` - Optionally, PlotItem my also be initialized with the keyword arguments left, - right, top, or bottom to achieve the same effect. - *name* Registers a name for this view so that others may link to it - ============= ========================================================================================== - - + The name of each axis and the corresponding arguments are passed to + :func:`PlotItem.setLabel() ` + Optionally, PlotItem my also be initialized with the keyword arguments left, + right, top, or bottom to achieve the same effect. + *name* Registers a name for this view so that others may link to it + *viewBox* If specified, the PlotItem will be constructed with this as its ViewBox. + *axisItems* Optional dictionary instructing the PlotItem to use pre-constructed items + for its axes. The dict keys must be axis names ('left', 'bottom', 'right', 'top') + and the values must be instances of AxisItem (or at least compatible with AxisItem). + ============== ========================================================================================== """ GraphicsWidget.__init__(self, parent) @@ -127,8 +124,6 @@ class PlotItem(GraphicsWidget): ## Set up control buttons path = os.path.dirname(__file__) - #self.ctrlBtn = ButtonItem(os.path.join(path, 'ctrl.png'), 14, self) - #self.ctrlBtn.clicked.connect(self.ctrlBtnClicked) self.autoImageFile = os.path.join(path, 'auto.png') self.lockImageFile = os.path.join(path, 'lock.png') self.autoBtn = ButtonItem(self.autoImageFile, 14, self) @@ -141,32 +136,33 @@ class PlotItem(GraphicsWidget): self.layout.setHorizontalSpacing(0) self.layout.setVerticalSpacing(0) - self.vb = ViewBox(name=name) + if viewBox is None: + viewBox = ViewBox() + self.vb = viewBox + self.setMenuEnabled(enableMenu, enableMenu) ## en/disable plotitem and viewbox menus + + if name is not None: + self.vb.register(name) self.vb.sigRangeChanged.connect(self.sigRangeChanged) self.vb.sigXRangeChanged.connect(self.sigXRangeChanged) self.vb.sigYRangeChanged.connect(self.sigYRangeChanged) - #self.vb.sigRangeChangedManually.connect(self.enableManualScale) - #self.vb.sigRangeChanged.connect(self.viewRangeChanged) self.layout.addItem(self.vb, 2, 1) self.alpha = 1.0 self.autoAlpha = True self.spectrumMode = False - #self.autoScale = [True, True] - - ## Create and place scale items - self.scales = { - 'top': {'item': AxisItem(orientation='top', linkView=self.vb), 'pos': (1, 1)}, - 'bottom': {'item': AxisItem(orientation='bottom', linkView=self.vb), 'pos': (3, 1)}, - 'left': {'item': AxisItem(orientation='left', linkView=self.vb), 'pos': (2, 0)}, - 'right': {'item': AxisItem(orientation='right', linkView=self.vb), 'pos': (2, 2)} - } - for k in self.scales: - item = self.scales[k]['item'] - self.layout.addItem(item, *self.scales[k]['pos']) - item.setZValue(-1000) - item.setFlag(item.ItemNegativeZStacksBehindParent) + ## Create and place axis items + if axisItems is None: + axisItems = {} + self.axes = {} + for k, pos in (('top', (1,1)), ('bottom', (3,1)), ('left', (2,0)), ('right', (2,2))): + axis = axisItems.get(k, AxisItem(orientation=k)) + axis.linkToView(self.vb) + self.axes[k] = {'item': axis, 'pos': pos} + self.layout.addItem(axis, *pos) + axis.setZValue(-1000) + axis.setFlag(axis.ItemNegativeZStacksBehindParent) self.titleLabel = LabelItem('', size='11pt') self.layout.addItem(self.titleLabel, 0, 1) @@ -193,7 +189,6 @@ class PlotItem(GraphicsWidget): 'setXRange', 'setYRange', 'setXLink', 'setYLink', 'setAutoPan', 'setAutoVisible', 'setRange', 'autoRange', 'viewRect', 'viewRange', 'setMouseEnabled', 'enableAutoRange', 'disableAutoRange', 'setAspectLocked', - 'setMenuEnabled', 'menuEnabled', 'register', 'unregister']: ## NOTE: If you update this list, please update the class docstring as well. setattr(self, m, getattr(self.vb, m)) @@ -233,45 +228,12 @@ class PlotItem(GraphicsWidget): self.subMenus.append(sm) self.ctrlMenu.addMenu(sm) - ## exporting is handled by GraphicsScene now - #exportOpts = collections.OrderedDict([ - #('SVG - Full Plot', self.saveSvgClicked), - #('SVG - Curves Only', self.saveSvgCurvesClicked), - #('Image', self.saveImgClicked), - #('CSV', self.saveCsvClicked), - #]) - - #self.vb.menu.setExportMethods(exportOpts) - - - #if HAVE_WIDGETGROUP: self.stateGroup = WidgetGroup() for name, w in menuItems: self.stateGroup.autoAdd(w) self.fileDialog = None - #self.xLinkPlot = None - #self.yLinkPlot = None - #self.linksBlocked = False - - #self.setAcceptHoverEvents(True) - - ## Connect control widgets - #c.xMinText.editingFinished.connect(self.setManualXScale) - #c.xMaxText.editingFinished.connect(self.setManualXScale) - #c.yMinText.editingFinished.connect(self.setManualYScale) - #c.yMaxText.editingFinished.connect(self.setManualYScale) - - #c.xManualRadio.clicked.connect(lambda: self.updateXScale()) - #c.yManualRadio.clicked.connect(lambda: self.updateYScale()) - - #c.xAutoRadio.clicked.connect(self.updateXScale) - #c.yAutoRadio.clicked.connect(self.updateYScale) - - #c.xAutoPercentSpin.valueChanged.connect(self.replot) - #c.yAutoPercentSpin.valueChanged.connect(self.replot) - c.alphaGroup.toggled.connect(self.updateAlpha) c.alphaSlider.valueChanged.connect(self.updateAlpha) c.autoAlphaCheck.toggled.connect(self.updateAlpha) @@ -283,13 +245,6 @@ class PlotItem(GraphicsWidget): c.fftCheck.toggled.connect(self.updateSpectrumMode) c.logXCheck.toggled.connect(self.updateLogMode) c.logYCheck.toggled.connect(self.updateLogMode) - #c.saveSvgBtn.clicked.connect(self.saveSvgClicked) - #c.saveSvgCurvesBtn.clicked.connect(self.saveSvgCurvesClicked) - #c.saveImgBtn.clicked.connect(self.saveImgClicked) - #c.saveCsvBtn.clicked.connect(self.saveCsvClicked) - - #self.ctrl.xLinkCombo.currentIndexChanged.connect(self.xLinkComboChanged) - #self.ctrl.yLinkCombo.currentIndexChanged.connect(self.yLinkComboChanged) c.downsampleSpin.valueChanged.connect(self.updateDownsampling) @@ -298,24 +253,15 @@ class PlotItem(GraphicsWidget): self.ctrl.maxTracesCheck.toggled.connect(self.updateDecimation) self.ctrl.maxTracesSpin.valueChanged.connect(self.updateDecimation) - #c.xMouseCheck.toggled.connect(self.mouseCheckChanged) - #c.yMouseCheck.toggled.connect(self.mouseCheckChanged) - - #self.xLinkPlot = None - #self.yLinkPlot = None - #self.linksBlocked = False - self.manager = None self.hideAxis('right') self.hideAxis('top') self.showAxis('left') self.showAxis('bottom') - #if name is not None: - #self.registerPlot(name) if labels is None: labels = {} - for label in list(self.scales.keys()): + for label in list(self.axes.keys()): if label in kargs: labels[label] = kargs[label] del kargs[label] @@ -330,15 +276,16 @@ class PlotItem(GraphicsWidget): if len(kargs) > 0: self.plot(**kargs) - #self.enableAutoRange() def implements(self, interface=None): return interface in ['ViewBoxWrapper'] def getViewBox(self): - """Return the ViewBox within.""" + """Return the :class:`ViewBox ` contained within.""" return self.vb + + def setLogMode(self, x, y): """ Set log scaling for x and y axes. @@ -399,11 +346,11 @@ class PlotItem(GraphicsWidget): #self.autoBtn.setParent(None) #self.autoBtn = None - for k in self.scales: - i = self.scales[k]['item'] + for k in self.axes: + i = self.axes[k]['item'] i.close() - self.scales = None + self.axes = None self.scene().removeItem(self.vb) self.vb = None @@ -431,47 +378,6 @@ class PlotItem(GraphicsWidget): def registerPlot(self, name): ## for backward compatibility self.vb.register(name) - #self.name = name - #win = str(self.window()) - ##print "register", name, win - #if win not in PlotItem.managers: - #PlotItem.managers[win] = PlotWidgetManager() - #self.manager = PlotItem.managers[win] - #self.manager.addWidget(self, name) - ##QtCore.QObject.connect(self.manager, QtCore.SIGNAL('widgetListChanged'), self.updatePlotList) - #self.manager.sigWidgetListChanged.connect(self.updatePlotList) - #self.updatePlotList() - - #def updatePlotList(self): - #"""Update the list of all plotWidgets in the "link" combos""" - ##print "update plot list", self - #try: - #for sc in [self.ctrl.xLinkCombo, self.ctrl.yLinkCombo]: - #current = unicode(sc.currentText()) - #sc.blockSignals(True) - #try: - #sc.clear() - #sc.addItem("") - #if self.manager is not None: - #for w in self.manager.listWidgets(): - ##print w - #if w == self.name: - #continue - #sc.addItem(w) - #if w == current: - #sc.setCurrentIndex(sc.count()-1) - #finally: - #sc.blockSignals(False) - #if unicode(sc.currentText()) != current: - #sc.currentItemChanged.emit() - #except: - #import gc - #refs= gc.get_referrers(self) - #print " error during update of", self - #print " Referrers are:", refs - #raise - - def updateGrid(self, *args): alpha = self.ctrl.gridAlphaSlider.value() @@ -492,91 +398,6 @@ class PlotItem(GraphicsWidget): return wr - - - - #def viewRangeChanged(self, vb, range): - ##self.emit(QtCore.SIGNAL('viewChanged'), *args) - #self.sigRangeChanged.emit(self, range) - - #def blockLink(self, b): - #self.linksBlocked = b - - #def xLinkComboChanged(self): - #self.setXLink(str(self.ctrl.xLinkCombo.currentText())) - - #def yLinkComboChanged(self): - #self.setYLink(str(self.ctrl.yLinkCombo.currentText())) - - #def setXLink(self, plot=None): - #"""Link this plot's X axis to another plot (pass either the PlotItem/PlotWidget or the registered name of the plot)""" - #if isinstance(plot, basestring): - #if self.manager is None: - #return - #if self.xLinkPlot is not None: - #self.manager.unlinkX(self, self.xLinkPlot) - #plot = self.manager.getWidget(plot) - #if not isinstance(plot, PlotItem) and hasattr(plot, 'getPlotItem'): - #plot = plot.getPlotItem() - #self.xLinkPlot = plot - #if plot is not None: - #self.setManualXScale() - #self.manager.linkX(self, plot) - - - - #def setYLink(self, plot=None): - #"""Link this plot's Y axis to another plot (pass either the PlotItem/PlotWidget or the registered name of the plot)""" - #if isinstance(plot, basestring): - #if self.manager is None: - #return - #if self.yLinkPlot is not None: - #self.manager.unlinkY(self, self.yLinkPlot) - #plot = self.manager.getWidget(plot) - #if not isinstance(plot, PlotItem) and hasattr(plot, 'getPlotItem'): - #plot = plot.getPlotItem() - #self.yLinkPlot = plot - #if plot is not None: - #self.setManualYScale() - #self.manager.linkY(self, plot) - - #def linkXChanged(self, plot): - #"""Called when a linked plot has changed its X scale""" - ##print "update from", plot - #if self.linksBlocked: - #return - #pr = plot.vb.viewRect() - #pg = plot.viewGeometry() - #if pg is None: - ##print " return early" - #return - #sg = self.viewGeometry() - #upp = float(pr.width()) / pg.width() - #x1 = pr.left() + (sg.x()-pg.x()) * upp - #x2 = x1 + sg.width() * upp - #plot.blockLink(True) - #self.setManualXScale() - #self.setXRange(x1, x2, padding=0) - #plot.blockLink(False) - #self.replot() - - #def linkYChanged(self, plot): - #"""Called when a linked plot has changed its Y scale""" - #if self.linksBlocked: - #return - #pr = plot.vb.viewRect() - #pg = plot.vb.boundingRect() - #sg = self.vb.boundingRect() - #upp = float(pr.height()) / pg.height() - #y1 = pr.bottom() + (sg.y()-pg.y()) * upp - #y2 = y1 + sg.height() * upp - #plot.blockLink(True) - #self.setManualYScale() - #self.setYRange(y1, y2, padding=0) - #plot.blockLink(False) - #self.replot() - - def avgToggled(self, b): if b: self.recomputeAverages() @@ -650,50 +471,6 @@ class PlotItem(GraphicsWidget): else: plot.setData(x, y) - - #def mouseCheckChanged(self): - #state = [self.ctrl.xMouseCheck.isChecked(), self.ctrl.yMouseCheck.isChecked()] - #self.vb.setMouseEnabled(*state) - - #def xRangeChanged(self, _, range): - #if any(np.isnan(range)) or any(np.isinf(range)): - #raise Exception("yRange invalid: %s. Signal came from %s" % (str(range), str(self.sender()))) - #self.ctrl.xMinText.setText('%0.5g' % range[0]) - #self.ctrl.xMaxText.setText('%0.5g' % range[1]) - - ### automatically change unit scale - #maxVal = max(abs(range[0]), abs(range[1])) - #(scale, prefix) = fn.siScale(maxVal) - ##for l in ['top', 'bottom']: - ##if self.getLabel(l).isVisible(): - ##self.setLabel(l, unitPrefix=prefix) - ##self.getScale(l).setScale(scale) - ##else: - ##self.setLabel(l, unitPrefix='') - ##self.getScale(l).setScale(1.0) - - ##self.emit(QtCore.SIGNAL('xRangeChanged'), self, range) - #self.sigXRangeChanged.emit(self, range) - - #def yRangeChanged(self, _, range): - #if any(np.isnan(range)) or any(np.isinf(range)): - #raise Exception("yRange invalid: %s. Signal came from %s" % (str(range), str(self.sender()))) - #self.ctrl.yMinText.setText('%0.5g' % range[0]) - #self.ctrl.yMaxText.setText('%0.5g' % range[1]) - - ### automatically change unit scale - #maxVal = max(abs(range[0]), abs(range[1])) - #(scale, prefix) = fn.siScale(maxVal) - ##for l in ['left', 'right']: - ##if self.getLabel(l).isVisible(): - ##self.setLabel(l, unitPrefix=prefix) - ##self.getScale(l).setScale(scale) - ##else: - ##self.setLabel(l, unitPrefix='') - ##self.getScale(l).setScale(1.0) - ##self.emit(QtCore.SIGNAL('yRangeChanged'), self, range) - #self.sigYRangeChanged.emit(self, range) - def autoBtnClicked(self): if self.autoBtn.mode == 'auto': self.enableAutoRange() @@ -706,72 +483,6 @@ class PlotItem(GraphicsWidget): """ print("Warning: enableAutoScale is deprecated. Use enableAutoRange(axis, enable) instead.") self.vb.enableAutoRange(self.vb.XYAxes) - #self.ctrl.xAutoRadio.setChecked(True) - #self.ctrl.yAutoRadio.setChecked(True) - - #self.autoBtn.setImageFile(self.lockImageFile) - #self.autoBtn.mode = 'lock' - #self.updateXScale() - #self.updateYScale() - #self.replot() - - #def updateXScale(self): - #"""Set plot to autoscale or not depending on state of radio buttons""" - #if self.ctrl.xManualRadio.isChecked(): - #self.setManualXScale() - #else: - #self.setAutoXScale() - #self.replot() - - #def updateYScale(self, b=False): - #"""Set plot to autoscale or not depending on state of radio buttons""" - #if self.ctrl.yManualRadio.isChecked(): - #self.setManualYScale() - #else: - #self.setAutoYScale() - #self.replot() - - #def enableManualScale(self, v=[True, True]): - #if v[0]: - #self.autoScale[0] = False - #self.ctrl.xManualRadio.setChecked(True) - ##self.setManualXScale() - #if v[1]: - #self.autoScale[1] = False - #self.ctrl.yManualRadio.setChecked(True) - ##self.setManualYScale() - ##self.autoBtn.enable() - #self.autoBtn.setImageFile(self.autoImageFile) - #self.autoBtn.mode = 'auto' - ##self.replot() - - #def setManualXScale(self): - #self.autoScale[0] = False - #x1 = float(self.ctrl.xMinText.text()) - #x2 = float(self.ctrl.xMaxText.text()) - #self.ctrl.xManualRadio.setChecked(True) - #self.setXRange(x1, x2, padding=0) - #self.autoBtn.show() - ##self.replot() - - #def setManualYScale(self): - #self.autoScale[1] = False - #y1 = float(self.ctrl.yMinText.text()) - #y2 = float(self.ctrl.yMaxText.text()) - #self.ctrl.yManualRadio.setChecked(True) - #self.setYRange(y1, y2, padding=0) - #self.autoBtn.show() - ##self.replot() - - #def setAutoXScale(self): - #self.autoScale[0] = True - #self.ctrl.xAutoRadio.setChecked(True) - ##self.replot() - - #def setAutoYScale(self): - #self.autoScale[1] = True - #self.ctrl.yAutoRadio.setChecked(True) - ##self.replot() def addItem(self, item, *args, **kargs): """ @@ -867,17 +578,6 @@ class PlotItem(GraphicsWidget): """ - - #if y is not None: - #data = y - #if data2 is not None: - #x = data - #data = data2 - #if decimate is not None and decimate > 1: - #data = data[::decimate] - #if x is not None: - #x = x[::decimate] - ## print 'plot with decimate = %d' % (decimate) clear = kargs.get('clear', False) params = kargs.get('params', None) @@ -888,23 +588,7 @@ class PlotItem(GraphicsWidget): if params is None: params = {} - #if HAVE_METAARRAY and (hasattr(data, 'implements') and data.implements('MetaArray')): - #curve = self._plotMetaArray(data, x=x, **kargs) - #elif isinstance(data, np.ndarray): - #curve = self._plotArray(data, x=x, **kargs) - #elif isinstance(data, list): - #if x is not None: - #x = np.array(x) - #curve = self._plotArray(np.array(data), x=x, **kargs) - #elif data is None: - #curve = PlotCurveItem(**kargs) - #else: - #raise Exception('Not sure how to plot object of type %s' % type(data)) - - #print data, curve self.addItem(item, params=params) - #if pen is not None: - #curve.setPen(fn.mkPen(pen)) return item @@ -922,80 +606,34 @@ class PlotItem(GraphicsWidget): del kargs['size'] return self.plot(*args, **kargs) - #sp = ScatterPlotItem(*args, **kargs) - #self.addItem(sp) - #return sp - - - - #def plotChanged(self, curve=None): - ## Recompute auto range if needed - #args = {} - #for ax in [0, 1]: - #print "range", ax - #if self.autoScale[ax]: - #percentScale = [self.ctrl.xAutoPercentSpin.value(), self.ctrl.yAutoPercentSpin.value()][ax] * 0.01 - #mn = None - #mx = None - #for c in self.curves + [c[1] for c in self.avgCurves.values()] + self.dataItems: - #if not c.isVisible(): - #continue - #cmn, cmx = c.getRange(ax, percentScale) - ##print " ", c, cmn, cmx - #if mn is None or cmn < mn: - #mn = cmn - #if mx is None or cmx > mx: - #mx = cmx - #if mn is None or mx is None or any(np.isnan([mn, mx])) or any(np.isinf([mn, mx])): - #continue - #if mn == mx: - #mn -= 1 - #mx += 1 - #if ax == 0: - #args['xRange'] = [mn, mx] - #else: - #args['yRange'] = [mn, mx] - - #if len(args) > 0: - ##print args - #self.setRange(**args) def replot(self): - #self.plotChanged() self.update() def updateParamList(self): self.ctrl.avgParamList.clear() ## Check to see that each parameter for each curve is present in the list - #print "\nUpdate param list", self - #print "paramList:", self.paramList for c in self.curves: - #print " curve:", c for p in list(self.itemMeta.get(c, {}).keys()): - #print " param:", p if type(p) is tuple: p = '.'.join(p) ## If the parameter is not in the list, add it. matches = self.ctrl.avgParamList.findItems(p, QtCore.Qt.MatchExactly) - #print " matches:", matches if len(matches) == 0: i = QtGui.QListWidgetItem(p) if p in self.paramList and self.paramList[p] is True: - #print " set checked" i.setCheckState(QtCore.Qt.Checked) else: - #print " set unchecked" i.setCheckState(QtCore.Qt.Unchecked) self.ctrl.avgParamList.addItem(i) else: i = matches[0] self.paramList[p] = (i.checkState() == QtCore.Qt.Checked) - #print "paramList:", self.paramList - ## This is bullshit. + ## Qt's SVG-writing capabilities are pretty terrible. def writeSvgCurves(self, fileName=None): if fileName is None: self.fileDialog = FileDialog() @@ -1190,18 +828,12 @@ class PlotItem(GraphicsWidget): def saveState(self): - #if not HAVE_WIDGETGROUP: - #raise Exception("State save/restore requires WidgetGroup class.") state = self.stateGroup.state() state['paramList'] = self.paramList.copy() state['view'] = self.vb.getState() - #print "\nSAVE %s:\n" % str(self.name), state - #print "Saving state. averageGroup.isChecked(): %s state: %s" % (str(self.ctrl.averageGroup.isChecked()), str(state['averageGroup'])) return state def restoreState(self, state): - #if not HAVE_WIDGETGROUP: - #raise Exception("State save/restore requires WidgetGroup class.") if 'paramList' in state: self.paramList = state['paramList'].copy() @@ -1218,8 +850,6 @@ class PlotItem(GraphicsWidget): state['yGridCheck'] = state['gridGroup'] self.stateGroup.setState(state) - #self.updateXScale() - #self.updateYScale() self.updateParamList() if 'view' not in state: @@ -1232,13 +862,6 @@ class PlotItem(GraphicsWidget): } self.vb.setState(state['view']) - - #print "\nRESTORE %s:\n" % str(self.name), state - #print "Restoring state. averageGroup.isChecked(): %s state: %s" % (str(self.ctrl.averageGroup.isChecked()), str(state['averageGroup'])) - #avg = self.ctrl.averageGroup.isChecked() - #if avg != state['averageGroup']: - #print " WARNING: avgGroup is %s, should be %s" % (str(avg), str(state['averageGroup'])) - def widgetGroupInterface(self): return (None, PlotItem.saveState, PlotItem.restoreState) @@ -1269,8 +892,6 @@ class PlotItem(GraphicsWidget): for c in self.curves: c.setDownsampling(ds) self.recomputeAverages() - #for c in self.avgCurves.values(): - #c[1].setDownsampling(ds) def downsampleMode(self): @@ -1306,8 +927,6 @@ class PlotItem(GraphicsWidget): (alpha, auto) = self.alphaState() for c in self.curves: c.setAlpha(alpha**2, auto) - - #self.replot(autoRange=False) def alphaState(self): enabled = self.ctrl.alphaGroup.isChecked() @@ -1330,9 +949,6 @@ class PlotItem(GraphicsWidget): mode = False return mode - #def wheelEvent(self, ev): - ## disables default panning the whole scene by mousewheel - #ev.accept() def resizeEvent(self, ev): if self.autoBtn is None: ## already closed down @@ -1340,29 +956,42 @@ class PlotItem(GraphicsWidget): btnRect = self.mapRectFromItem(self.autoBtn, self.autoBtn.boundingRect()) y = self.size().height() - btnRect.height() self.autoBtn.setPos(0, y) - - #def hoverMoveEvent(self, ev): - #self.mousePos = ev.pos() - #self.mouseScreenPos = ev.screenPos() - - - #def ctrlBtnClicked(self): - #self.ctrlMenu.popup(self.mouseScreenPos) - + + def getMenu(self): return self.ctrlMenu def getContextMenus(self, event): ## called when another item is displaying its context menu; we get to add extras to the end of the menu. - return self.ctrlMenu - + if self.menuEnabled(): + return self.ctrlMenu + else: + return None + + def setMenuEnabled(self, enableMenu=True, enableViewBoxMenu='same'): + """ + Enable or disable the context menu for this PlotItem. + By default, the ViewBox's context menu will also be affected. + (use enableViewBoxMenu=None to leave the ViewBox unchanged) + """ + self._menuEnabled = enableMenu + if enableViewBoxMenu is None: + return + if enableViewBoxMenu is 'same': + enableViewBoxMenu = enableMenu + self.vb.setMenuEnabled(enableViewBoxMenu) + + def menuEnabled(self): + return self._menuEnabled + + def getLabel(self, key): pass def _checkScaleKey(self, key): - if key not in self.scales: - raise Exception("Scale '%s' not found. Scales are: %s" % (key, str(list(self.scales.keys())))) + if key not in self.axes: + raise Exception("Scale '%s' not found. Scales are: %s" % (key, str(list(self.axes.keys())))) def getScale(self, key): return self.getAxis(key) @@ -1371,7 +1000,7 @@ class PlotItem(GraphicsWidget): """Return the specified AxisItem. *name* should be 'left', 'bottom', 'top', or 'right'.""" self._checkScaleKey(name) - return self.scales[name]['item'] + return self.axes[name]['item'] def setLabel(self, axis, text=None, units=None, unitPrefix=None, **args): """ @@ -1417,7 +1046,7 @@ class PlotItem(GraphicsWidget): axis must be one of 'left', 'bottom', 'right', or 'top' """ s = self.getScale(axis) - p = self.scales[axis]['pos'] + p = self.axes[axis]['pos'] if show: s.show() else: @@ -1454,7 +1083,6 @@ class PlotItem(GraphicsWidget): ## create curve try: xv = arr.xvals(0) - #print 'xvals:', xv except: if x is None: xv = np.arange(arr.shape[0]) @@ -1474,17 +1102,6 @@ class PlotItem(GraphicsWidget): return c - #def saveSvgClicked(self): - #self.writeSvg() - - #def saveSvgCurvesClicked(self): - #self.writeSvgCurves() - - #def saveImgClicked(self): - #self.writeImage() - - #def saveCsvClicked(self): - #self.writeCsv() def setExportMode(self, export, opts): if export: @@ -1492,63 +1109,3 @@ class PlotItem(GraphicsWidget): else: self.autoBtn.show() - -#class PlotWidgetManager(QtCore.QObject): - - #sigWidgetListChanged = QtCore.Signal(object) - - #"""Used for managing communication between PlotWidgets""" - #def __init__(self): - #QtCore.QObject.__init__(self) - #self.widgets = weakref.WeakValueDictionary() # Don't keep PlotWidgets around just because they are listed here - - #def addWidget(self, w, name): - #self.widgets[name] = w - ##self.emit(QtCore.SIGNAL('widgetListChanged'), self.widgets.keys()) - #self.sigWidgetListChanged.emit(self.widgets.keys()) - - #def removeWidget(self, name): - #if name in self.widgets: - #del self.widgets[name] - ##self.emit(QtCore.SIGNAL('widgetListChanged'), self.widgets.keys()) - #self.sigWidgetListChanged.emit(self.widgets.keys()) - #else: - #print "plot %s not managed" % name - - - #def listWidgets(self): - #return self.widgets.keys() - - #def getWidget(self, name): - #if name not in self.widgets: - #return None - #else: - #return self.widgets[name] - - #def linkX(self, p1, p2): - ##QtCore.QObject.connect(p1, QtCore.SIGNAL('xRangeChanged'), p2.linkXChanged) - #p1.sigXRangeChanged.connect(p2.linkXChanged) - ##QtCore.QObject.connect(p2, QtCore.SIGNAL('xRangeChanged'), p1.linkXChanged) - #p2.sigXRangeChanged.connect(p1.linkXChanged) - #p1.linkXChanged(p2) - ##p2.setManualXScale() - - #def unlinkX(self, p1, p2): - ##QtCore.QObject.disconnect(p1, QtCore.SIGNAL('xRangeChanged'), p2.linkXChanged) - #p1.sigXRangeChanged.disconnect(p2.linkXChanged) - ##QtCore.QObject.disconnect(p2, QtCore.SIGNAL('xRangeChanged'), p1.linkXChanged) - #p2.sigXRangeChanged.disconnect(p1.linkXChanged) - - #def linkY(self, p1, p2): - ##QtCore.QObject.connect(p1, QtCore.SIGNAL('yRangeChanged'), p2.linkYChanged) - #p1.sigYRangeChanged.connect(p2.linkYChanged) - ##QtCore.QObject.connect(p2, QtCore.SIGNAL('yRangeChanged'), p1.linkYChanged) - #p2.sigYRangeChanged.connect(p1.linkYChanged) - #p1.linkYChanged(p2) - ##p2.setManualYScale() - - #def unlinkY(self, p1, p2): - ##QtCore.QObject.disconnect(p1, QtCore.SIGNAL('yRangeChanged'), p2.linkYChanged) - #p1.sigYRangeChanged.disconnect(p2.linkYChanged) - ##QtCore.QObject.disconnect(p2, QtCore.SIGNAL('yRangeChanged'), p1.linkYChanged) - #p2.sigYRangeChanged.disconnect(p1.linkYChanged) diff --git a/graphicsItems/ViewBox/ViewBox.py b/graphicsItems/ViewBox/ViewBox.py index 398b1e8f..694455e5 100644 --- a/graphicsItems/ViewBox/ViewBox.py +++ b/graphicsItems/ViewBox/ViewBox.py @@ -62,7 +62,7 @@ class ViewBox(GraphicsWidget): NamedViews = weakref.WeakValueDictionary() # name: ViewBox AllViews = weakref.WeakKeyDictionary() # ViewBox: None - def __init__(self, parent=None, border=None, lockAspect=False, enableMouse=True, invertY=False, enableMenu = True, name=None): + def __init__(self, parent=None, border=None, lockAspect=False, enableMouse=True, invertY=False, enableMenu=True, name=None): """ ============= ============================================================= **Arguments** @@ -136,7 +136,7 @@ class ViewBox(GraphicsWidget): ## Make scale box that is shown when dragging on the view self.rbScaleBox = QtGui.QGraphicsRectItem(0, 0, 1, 1) - self.rbScaleBox.setPen(fn.mkPen((255,0,0), width=1)) + self.rbScaleBox.setPen(fn.mkPen((255,255,100), width=1)) self.rbScaleBox.setBrush(fn.mkBrush(255,255,0,100)) self.rbScaleBox.hide() self.addItem(self.rbScaleBox) @@ -358,7 +358,7 @@ class ViewBox(GraphicsWidget): changes[1] = yRange if len(changes) == 0: - print rect + print(rect) raise Exception("Must specify at least one of rect, xRange, or yRange. (gave rect=%s)" % str(type(rect))) changed = [False, False] @@ -863,7 +863,10 @@ class ViewBox(GraphicsWidget): return self._menuCopy def getContextMenus(self, event): - return self.menu.subMenus() + if self.menuEnabled(): + return self.menu.subMenus() + else: + return None #return [self.getMenu(event)] diff --git a/widgets/PlotWidget.py b/widgets/PlotWidget.py index 82e4701d..1fa07f2a 100644 --- a/widgets/PlotWidget.py +++ b/widgets/PlotWidget.py @@ -41,6 +41,8 @@ class PlotWidget(GraphicsView): other methods, use :func:`getPlotItem `. """ def __init__(self, parent=None, **kargs): + """When initializing PlotWidget, all keyword arguments except *parent* are passed + to :func:`PlotItem.__init__() `.""" GraphicsView.__init__(self, parent) self.setSizePolicy(QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Expanding) self.enableMouse(False)