More PlotItem cleanup

This commit is contained in:
Luke Campagnola 2018-06-08 08:43:46 -07:00
parent 7358664414
commit 7be7c3f459

View File

@ -1,21 +1,4 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
"""
PlotItem.py - Graphics item implementing a scalable ViewBox with plotting powers.
Copyright 2010 Luke Campagnola
Distributed under MIT/X11 license. See license.txt for more infomation.
This class is one of the workhorses of pyqtgraph. It implements a graphics item with
plots, labels, and scales which can be viewed inside a QGraphicsScene. If you want
a widget that can be added to your GUI, see PlotWidget instead.
This class is very heavily featured:
- Automatically creates and manages PlotCurveItems
- Fast display and update of plots
- Manages zoom/pan ViewBox, scale, and label elements
- Automatic scaling when data changes
- Control panel with a huge feature set including averaging, decimation,
display, power spectrum, svg/png export, plot linking, and more.
"""
import sys import sys
import weakref import weakref
import numpy as np import numpy as np
@ -53,17 +36,24 @@ except:
HAVE_METAARRAY = False HAVE_METAARRAY = False
class PlotItem(GraphicsWidget): class PlotItem(GraphicsWidget):
"""GraphicsWidget implementing a standard 2D plotting area with axes.
"""
**Bases:** :class:`GraphicsWidget <pyqtgraph.GraphicsWidget>` **Bases:** :class:`GraphicsWidget <pyqtgraph.GraphicsWidget>`
Plot graphics item that can be added to any graphics scene. Implements axes, titles, and interactive viewbox. This class provides the ViewBox-plus-axes that appear when using
PlotItem also provides some basic analysis functionality that may be accessed from the context menu. :func:`pg.plot() <pyqtgraph.plot>`, :class:`PlotWidget <pyqtgraph.PlotWidget>`,
Use :func:`plot() <pyqtgraph.PlotItem.plot>` to create a new PlotDataItem and add it to the view. and :func:`GraphicsLayoutWidget.addPlot() <pyqtgraph.GraphicsLayoutWidget.addPlot>`.
Use :func:`addItem() <pyqtgraph.PlotItem.addItem>` to add any QGraphicsItem to the view.
It's main functionality is:
- Manage placement of ViewBox, AxisItems, and LabelItems
- Create and manage a list of PlotDataItems displayed inside the ViewBox
- Implement a context menu with commonly used display and analysis options
Use :func:`plot() <pyqtgraph.PlotItem.plot>` to create a new PlotDataItem and
add it to the view. Use :func:`addItem() <pyqtgraph.PlotItem.addItem>` to
add any QGraphicsItem to the view.
This class wraps several methods from its internal ViewBox: This class wraps several methods from its internal ViewBox:
:func:`setXRange <pyqtgraph.ViewBox.setXRange>`, :func:`setXRange <pyqtgraph.ViewBox.setXRange>`,
@ -100,7 +90,6 @@ class PlotItem(GraphicsWidget):
sigYRangeChanged = QtCore.Signal(object, object) ## Emitted when the ViewBox Y range has changed sigYRangeChanged = QtCore.Signal(object, object) ## Emitted when the ViewBox Y range has changed
sigXRangeChanged = QtCore.Signal(object, object) ## Emitted when the ViewBox X range has changed sigXRangeChanged = QtCore.Signal(object, object) ## Emitted when the ViewBox X range has changed
lastFileDir = None lastFileDir = None
def __init__(self, parent=None, name=None, labels=None, title=None, viewBox=None, axisItems=None, enableMenu=True, **kargs): def __init__(self, parent=None, name=None, labels=None, title=None, viewBox=None, axisItems=None, enableMenu=True, **kargs):
@ -133,12 +122,9 @@ class PlotItem(GraphicsWidget):
## Set up control buttons ## Set up control buttons
path = os.path.dirname(__file__) path = os.path.dirname(__file__)
#self.autoImageFile = os.path.join(path, 'auto.png')
#self.lockImageFile = os.path.join(path, 'lock.png')
self.autoBtn = ButtonItem(pixmaps.getPixmap('auto'), 14, self) self.autoBtn = ButtonItem(pixmaps.getPixmap('auto'), 14, self)
self.autoBtn.mode = 'auto' self.autoBtn.mode = 'auto'
self.autoBtn.clicked.connect(self.autoBtnClicked) self.autoBtn.clicked.connect(self.autoBtnClicked)
#self.autoBtn.hide()
self.buttonsHidden = False ## whether the user has requested buttons to be hidden self.buttonsHidden = False ## whether the user has requested buttons to be hidden
self.mouseHovering = False self.mouseHovering = False
@ -186,7 +172,6 @@ class PlotItem(GraphicsWidget):
self.layout.addItem(self.titleLabel, 0, 1) self.layout.addItem(self.titleLabel, 0, 1)
self.setTitle(None) ## hide self.setTitle(None) ## hide
for i in range(4): for i in range(4):
self.layout.setRowPreferredHeight(i, 0) self.layout.setRowPreferredHeight(i, 0)
self.layout.setRowMinimumHeight(i, 0) self.layout.setRowMinimumHeight(i, 0)
@ -291,7 +276,6 @@ class PlotItem(GraphicsWidget):
if len(kargs) > 0: if len(kargs) > 0:
self.plot(**kargs) self.plot(**kargs)
def implements(self, interface=None): def implements(self, interface=None):
return interface in ['ViewBoxWrapper'] return interface in ['ViewBoxWrapper']
@ -299,11 +283,9 @@ class PlotItem(GraphicsWidget):
"""Return the :class:`ViewBox <pyqtgraph.ViewBox>` contained within.""" """Return the :class:`ViewBox <pyqtgraph.ViewBox>` contained within."""
return self.vb return self.vb
## Wrap a few methods from viewBox. ## Wrap a few methods from viewBox.
#Important: don't use a settattr(m, getattr(self.vb, m)) as we'd be leaving the viebox alive #Important: don't use a settattr(m, getattr(self.vb, m)) as we'd be leaving the viebox alive
#because we had a reference to an instance method (creating wrapper methods at runtime instead). #because we had a reference to an instance method (creating wrapper methods at runtime instead).
for m in ['setXRange', 'setYRange', 'setXLink', 'setYLink', 'setAutoPan', # NOTE: for m in ['setXRange', 'setYRange', 'setXLink', 'setYLink', 'setAutoPan', # NOTE:
'setAutoVisible', 'setRange', 'autoRange', 'viewRect', 'viewRange', # If you update this list, please 'setAutoVisible', 'setRange', 'autoRange', 'viewRect', 'viewRange', # If you update this list, please
'setMouseEnabled', 'setLimits', 'enableAutoRange', 'disableAutoRange', # update the class docstring 'setMouseEnabled', 'setLimits', 'enableAutoRange', 'disableAutoRange', # update the class docstring
@ -319,7 +301,6 @@ class PlotItem(GraphicsWidget):
del _create_method del _create_method
def setLogMode(self, x=None, y=None): def setLogMode(self, x=None, y=None):
""" """
Set log scaling for x and/or y axes. Set log scaling for x and/or y axes.
@ -359,7 +340,6 @@ class PlotItem(GraphicsWidget):
self.ctrl.gridAlphaSlider.setValue(v) self.ctrl.gridAlphaSlider.setValue(v)
def close(self): def close(self):
#print "delete", self
## Most of this crap is needed to avoid PySide trouble. ## Most of this crap is needed to avoid PySide trouble.
## The problem seems to be whenever scene.clear() leads to deletion of widgets (either through proxies or qgraphicswidgets) ## The problem seems to be whenever scene.clear() leads to deletion of widgets (either through proxies or qgraphicswidgets)
## the solution is to manually remove all widgets before scene.clear() is called ## the solution is to manually remove all widgets before scene.clear() is called
@ -400,7 +380,6 @@ class PlotItem(GraphicsWidget):
wr.adjust(pos.x(), pos.y(), pos.x(), pos.y()) wr.adjust(pos.x(), pos.y(), pos.x(), pos.y())
return wr return wr
def avgToggled(self, b): def avgToggled(self, b):
if b: if b:
self.recomputeAverages() self.recomputeAverages()
@ -543,7 +522,6 @@ class PlotItem(GraphicsWidget):
if name is not None and hasattr(self, 'legend') and self.legend is not None: if name is not None and hasattr(self, 'legend') and self.legend is not None:
self.legend.addItem(item, name=name) self.legend.addItem(item, name=name)
def addDataItem(self, item, *args): def addDataItem(self, item, *args):
print("PlotItem.addDataItem is deprecated. Use addItem instead.") print("PlotItem.addDataItem is deprecated. Use addItem instead.")
self.addItem(item, *args) self.addItem(item, *args)
@ -575,8 +553,6 @@ class PlotItem(GraphicsWidget):
line.setZValue(z) line.setZValue(z)
return line return line
def removeItem(self, item): def removeItem(self, item):
""" """
Remove an item from the internal ViewBox. Remove an item from the internal ViewBox.
@ -593,8 +569,6 @@ class PlotItem(GraphicsWidget):
self.curves.remove(item) self.curves.remove(item)
self.updateDecimation() self.updateDecimation()
self.updateParamList() self.updateParamList()
#item.connect(item, QtCore.SIGNAL('plotChanged'), self.plotChanged)
#item.sigPlotChanged.connect(self.plotChanged)
if self.legend is not None: if self.legend is not None:
self.legend.removeItem(item) self.legend.removeItem(item)
@ -612,7 +586,6 @@ class PlotItem(GraphicsWidget):
self.removeItem(i) self.removeItem(i)
self.avgCurves = {} self.avgCurves = {}
def plot(self, *args, **kargs): def plot(self, *args, **kargs):
""" """
Add and return a new plot. Add and return a new plot.
@ -622,8 +595,6 @@ class PlotItem(GraphicsWidget):
clear - clear all plots before displaying new data clear - clear all plots before displaying new data
params - meta-parameters to associate with this data params - meta-parameters to associate with this data
""" """
clear = kargs.get('clear', False) clear = kargs.get('clear', False)
params = kargs.get('params', None) params = kargs.get('params', None)
@ -692,11 +663,9 @@ class PlotItem(GraphicsWidget):
self.paramList[p] = (i.checkState() == QtCore.Qt.Checked) self.paramList[p] = (i.checkState() == QtCore.Qt.Checked)
## Qt's SVG-writing capabilities are pretty terrible.
def writeSvgCurves(self, fileName=None): def writeSvgCurves(self, fileName=None):
if fileName is None: if fileName is None:
self._choose_filename_dialog(handler=self.writeSvg) self._chooseFilenameDialog(handler=self.writeSvg)
return return
if isinstance(fileName, tuple): if isinstance(fileName, tuple):
@ -722,12 +691,10 @@ class PlotItem(GraphicsWidget):
sy *= 1000 sy *= 1000
sy *= -1 sy *= -1
#fh.write('<svg viewBox="%f %f %f %f">\n' % (rect.left()*sx, rect.top()*sx, rect.width()*sy, rect.height()*sy))
fh.write('<svg>\n') fh.write('<svg>\n')
fh.write('<path fill="none" stroke="#000000" stroke-opacity="0.5" stroke-width="1" d="M%f,0 L%f,0"/>\n' % (rect.left()*sx, rect.right()*sx)) fh.write('<path fill="none" stroke="#000000" stroke-opacity="0.5" stroke-width="1" d="M%f,0 L%f,0"/>\n' % (rect.left()*sx, rect.right()*sx))
fh.write('<path fill="none" stroke="#000000" stroke-opacity="0.5" stroke-width="1" d="M0,%f L0,%f"/>\n' % (rect.top()*sy, rect.bottom()*sy)) fh.write('<path fill="none" stroke="#000000" stroke-opacity="0.5" stroke-width="1" d="M0,%f L0,%f"/>\n' % (rect.top()*sy, rect.bottom()*sy))
for item in self.curves: for item in self.curves:
if isinstance(item, PlotCurveItem): if isinstance(item, PlotCurveItem):
color = fn.colorStr(item.pen.color()) color = fn.colorStr(item.pen.color())
@ -744,13 +711,12 @@ class PlotItem(GraphicsWidget):
x *= sx x *= sx
y *= sy y *= sy
#fh.write('<g fill="none" stroke="#%s" stroke-opacity="1" stroke-width="1">\n' % color)
fh.write('<path fill="none" stroke="#%s" stroke-opacity="%f" stroke-width="1" d="M%f,%f ' % (color, opacity, x[0], y[0])) fh.write('<path fill="none" stroke="#%s" stroke-opacity="%f" stroke-width="1" d="M%f,%f ' % (color, opacity, x[0], y[0]))
for i in range(1, len(x)): for i in range(1, len(x)):
fh.write('L%f,%f ' % (x[i], y[i])) fh.write('L%f,%f ' % (x[i], y[i]))
fh.write('"/>') fh.write('"/>')
#fh.write("</g>")
for item in self.dataItems: for item in self.dataItems:
if isinstance(item, ScatterPlotItem): if isinstance(item, ScatterPlotItem):
@ -771,11 +737,9 @@ class PlotItem(GraphicsWidget):
fh.write("</svg>\n") fh.write("</svg>\n")
def writeSvg(self, fileName=None): def writeSvg(self, fileName=None):
if fileName is None: if fileName is None:
self._choose_filename_dialog(handler=self.writeSvg) self._chooseFilenameDialog(handler=self.writeSvg)
return return
fileName = str(fileName) fileName = str(fileName)
@ -787,23 +751,16 @@ class PlotItem(GraphicsWidget):
def writeImage(self, fileName=None): def writeImage(self, fileName=None):
if fileName is None: if fileName is None:
self._choose_filename_dialog(handler=self.writeImage) self._chooseFilenameDialog(handler=self.writeImage)
return return
if isinstance(fileName, tuple): from ...exporters import ImageExporter
raise Exception("Not implemented yet..") ex = ImageExporter(self)
fileName = str(fileName) ex.export(fileName)
PlotItem.lastFileDir = os.path.dirname(fileName)
self.png = QtGui.QImage(int(self.size().width()), int(self.size().height()), QtGui.QImage.Format_ARGB32)
painter = QtGui.QPainter(self.png)
painter.setRenderHints(painter.Antialiasing | painter.TextAntialiasing)
self.scene().render(painter, QtCore.QRectF(), self.mapRectToScene(self.boundingRect()))
painter.end()
self.png.save(fileName)
def writeCsv(self, fileName=None): def writeCsv(self, fileName=None):
if fileName is None: if fileName is None:
self._choose_filename_dialog(handler=self.writeCsv) self._chooseFilenameDialog(handler=self.writeCsv)
return return
fileName = str(fileName) fileName = str(fileName)
@ -826,7 +783,6 @@ class PlotItem(GraphicsWidget):
i += 1 i += 1
fd.close() fd.close()
def saveState(self): def saveState(self):
state = self.stateGroup.state() state = self.stateGroup.state()
state['paramList'] = self.paramList.copy() state['paramList'] = self.paramList.copy()
@ -862,7 +818,6 @@ class PlotItem(GraphicsWidget):
} }
self.vb.setState(state['view']) self.vb.setState(state['view'])
def widgetGroupInterface(self): def widgetGroupInterface(self):
return (None, PlotItem.saveState, PlotItem.restoreState) return (None, PlotItem.saveState, PlotItem.restoreState)
@ -961,8 +916,6 @@ class PlotItem(GraphicsWidget):
def clipToViewMode(self): def clipToViewMode(self):
return self.ctrl.clipToViewCheck.isChecked() return self.ctrl.clipToViewCheck.isChecked()
def updateDecimation(self): def updateDecimation(self):
if self.ctrl.maxTracesCheck.isChecked(): if self.ctrl.maxTracesCheck.isChecked():
numCurves = self.ctrl.maxTracesSpin.value() numCurves = self.ctrl.maxTracesSpin.value()
@ -981,7 +934,6 @@ class PlotItem(GraphicsWidget):
else: else:
curves[i].hide() curves[i].hide()
def updateAlpha(self, *args): def updateAlpha(self, *args):
(alpha, auto) = self.alphaState() (alpha, auto) = self.alphaState()
for c in self.curves: for c in self.curves:
@ -1008,7 +960,6 @@ class PlotItem(GraphicsWidget):
mode = False mode = False
return mode return mode
def resizeEvent(self, ev): def resizeEvent(self, ev):
if self.autoBtn is None: ## already closed down if self.autoBtn is None: ## already closed down
return return
@ -1016,7 +967,6 @@ class PlotItem(GraphicsWidget):
y = self.size().height() - btnRect.height() y = self.size().height() - btnRect.height()
self.autoBtn.setPos(0, y) self.autoBtn.setPos(0, y)
def getMenu(self): def getMenu(self):
return self.ctrlMenu return self.ctrlMenu
@ -1051,7 +1001,6 @@ class PlotItem(GraphicsWidget):
self.updateButtons() self.updateButtons()
def getLabel(self, key): def getLabel(self, key):
pass pass
@ -1099,7 +1048,6 @@ class PlotItem(GraphicsWidget):
v = (v,) v = (v,)
self.setLabel(k, *v) self.setLabel(k, *v)
def showLabel(self, axis, show=True): def showLabel(self, axis, show=True):
""" """
Show or hide one of the plot's axis labels (the axis itself will be unaffected). Show or hide one of the plot's axis labels (the axis itself will be unaffected).
@ -1173,8 +1121,6 @@ class PlotItem(GraphicsWidget):
c = PlotCurveItem(arr, x=x, **kargs) c = PlotCurveItem(arr, x=x, **kargs)
return c return c
def _plotMetaArray(self, arr, x=None, autoLabel=True, **kargs): def _plotMetaArray(self, arr, x=None, autoLabel=True, **kargs):
inf = arr.infoCopy() inf = arr.infoCopy()
if arr.ndim != 1: if arr.ndim != 1:
@ -1201,16 +1147,11 @@ class PlotItem(GraphicsWidget):
return c return c
def setExportMode(self, export, opts=None): def setExportMode(self, export, opts=None):
GraphicsWidget.setExportMode(self, export, opts) GraphicsWidget.setExportMode(self, export, opts)
self.updateButtons() self.updateButtons()
#if export:
#self.autoBtn.hide()
#else:
#self.autoBtn.show()
def _choose_filename_dialog(self, handler): def _chooseFilenameDialog(self, handler):
self.fileDialog = FileDialog() self.fileDialog = FileDialog()
if PlotItem.lastFileDir is not None: if PlotItem.lastFileDir is not None:
self.fileDialog.setDirectory(PlotItem.lastFileDir) self.fileDialog.setDirectory(PlotItem.lastFileDir)