QtCore = qtcompat.QtCore
QtGui = qtcompat.QtGui
+class Trace(object):
+ '''Class to hold the measured samples.'''
+
+ def __init__(self):
+ self.samples = []
+ self.new = False
+
+ def append(self, sample):
+ self.samples.append(sample)
+ self.new = True
+
class MeasurementDataModel(QtGui.QStandardItemModel):
'''Model to hold the measured values.'''
'''Role used to store the device vendor and model.'''
descRole = QtCore.Qt.UserRole + 2
- '''Role used to store past samples.'''
- samplesRole = QtCore.Qt.UserRole + 3
+ '''Role used to store a dictionary with the traces'''
+ tracesRole = QtCore.Qt.UserRole + 3
'''Role used to store the color to draw the graph of the channel.'''
colorRole = QtCore.Qt.UserRole + 4
item = QtGui.QStandardItem()
item.setData(uid, MeasurementDataModel.idRole)
item.setData(desc, MeasurementDataModel.descRole)
- item.setData(collections.defaultdict(list), MeasurementDataModel.samplesRole)
+ item.setData(collections.defaultdict(Trace), MeasurementDataModel.tracesRole)
item.setData(next(self._colorgen), MeasurementDataModel.colorRole)
self.appendRow(item)
self.sort(0)
# The samples role is a dictionary that contains the old samples for each unit.
# Should be trimmed periodically, otherwise it grows larger and larger.
sample = (time.time(), value)
- d = item.data(MeasurementDataModel.samplesRole)
- d[unit].append(sample)
+ traces = item.data(MeasurementDataModel.tracesRole)
+ traces[unit].append(sample)
class MultimeterDelegate(QtGui.QStyledItemDelegate):
'''Delegate to show the data items from a MeasurementDataModel.'''
self.listView.setMinimumSize(self.delegate.sizeHint())
self.plotwidget = multiplotwidget.MultiPlotWidget(self)
+ self.plotwidget.plotHidden.connect(self._on_plotHidden)
# Maps from 'unit' to the corresponding plot.
self._plots = {}
def timerEvent(self, event):
'''Periodically updates all graphs.'''
- for row in range(self.model.rowCount()):
- idx = self.model.index(row, 0)
- deviceID = self.model.data(idx, datamodel.MeasurementDataModel.idRole)
- sampledict = self.model.data(idx, datamodel.MeasurementDataModel.samplesRole)
- color = self.model.data(idx, datamodel.MeasurementDataModel.colorRole)
- for unit in sampledict:
- self._updatePlot(deviceID, unit, sampledict[unit], color)
-
- def _updatePlot(self, deviceID, unit, samples, color):
- '''Updates the curve of device 'deviceID' and 'unit' with 'samples',
- changes the color of the curve to 'color'.'''
-
- plot = self._getPlot(unit)
- curve = self._getCurve(plot, deviceID)
- curve.setPen(pyqtgraph.mkPen(color=color, width=1))
-
- now = time.time()
+ self._updatePlots()
- # remove old samples
- l = now - MainWindow.BACKLOG
- while samples and samples[0][0] < l:
- samples.pop(0)
+ def _updatePlots(self):
+ '''Updates all plots.'''
- xdata = [s[0] - now for s in samples]
- ydata = [s[1] for s in samples]
+ # loop over all devices and channels
+ for row in range(self.model.rowCount()):
+ idx = self.model.index(row, 0)
+ deviceID = self.model.data(idx,
+ datamodel.MeasurementDataModel.idRole)
+ traces = self.model.data(idx,
+ datamodel.MeasurementDataModel.tracesRole)
+
+ for unit, trace in traces.items():
+ now = time.time()
+
+ # remove old samples
+ l = now - MainWindow.BACKLOG
+ while trace.samples and trace.samples[0][0] < l:
+ trace.samples.pop(0)
+
+ plot = self._getPlot(unit)
+ if not plot.visible:
+ if trace.new:
+ self.plotwidget.showPlot(plot)
+
+ if plot.visible:
+ xdata = [s[0] - now for s in trace.samples]
+ ydata = [s[1] for s in trace.samples]
+
+ color = self.model.data(idx,
+ datamodel.MeasurementDataModel.colorRole)
+
+ curve = self._getCurve(plot, deviceID)
+ curve.setPen(pyqtgraph.mkPen(color=color))
+ curve.setData(xdata, ydata)
+
+ @QtCore.Slot(multiplotwidget.Plot)
+ def _on_plotHidden(self, plot):
+ plotunit = [u for u, p in self._plots.items() if p == plot][0]
+
+ # Mark all traces of all devices/channels with the same unit as the
+ # plot as "old" ('trace.new = False'). As soon as a new sample arrives
+ # on one trace, the plot will be shown again
+ for row in range(self.model.rowCount()):
+ idx = self.model.index(row, 0)
+ traces = self.model.data(idx, datamodel.MeasurementDataModel.tracesRole)
- curve.setData(xdata, ydata)
+ for traceunit, trace in traces.items():
+ if traceunit == plotunit:
+ trace.new = False
def closeEvent(self, event):
self.thread.stop()
pyqtgraph.setConfigOption('background', 'w')
pyqtgraph.setConfigOption('foreground', 'k')
+class Plot(object):
+ '''Helper class to keep all graphics items of a plot together.'''
+
+ def __init__(self, view, xaxis, yaxis):
+ self.view = view
+ self.xaxis = xaxis
+ self.yaxis = yaxis
+ self.visible = False
+
class MultiPlotItem(pyqtgraph.GraphicsWidget):
- class Plot:
- def __init__(self, view, xaxis, yaxis):
- self.view = view
- self.xaxis = xaxis
- self.yaxis = yaxis
+ # Emitted when a plot is shown.
+ plotShown = QtCore.Signal()
+
+ # Emitted when a plot is hidden by the user via the context menu.
+ plotHidden = QtCore.Signal(Plot)
def __init__(self, parent=None):
pyqtgraph.GraphicsWidget.__init__(self, parent)
- self.layout = QtGui.QGraphicsGridLayout()
- self.layout.setContentsMargins(10, 10, 10, 1)
- self.layout.setHorizontalSpacing(0)
- self.layout.setVerticalSpacing(0)
- self.setLayout(self.layout)
-
- self._plots = []
+ self.setLayout(QtGui.QGraphicsGridLayout())
+ self.layout().setContentsMargins(10, 10, 10, 1)
+ self.layout().setHorizontalSpacing(0)
+ self.layout().setVerticalSpacing(0)
for i in range(2):
- self.layout.setColumnPreferredWidth(i, 0)
- self.layout.setColumnMinimumWidth(i, 0)
- self.layout.setColumnSpacing(i, 0)
+ self.layout().setColumnPreferredWidth(i, 0)
+ self.layout().setColumnMinimumWidth(i, 0)
+ self.layout().setColumnSpacing(i, 0)
+
+ self.layout().setColumnStretchFactor(0, 0)
+ self.layout().setColumnStretchFactor(1, 100)
+
+ # List of 'Plot' objects that are shown.
+ self._plots = []
- self.layout.setColumnStretchFactor(0, 0)
- self.layout.setColumnStretchFactor(1, 100)
+ self._hideActions = {}
def addPlot(self):
- row = self.layout.rowCount()
+ '''Adds and returns a new plot.'''
+
+ row = self.layout().rowCount()
view = pyqtgraph.ViewBox(parent=self)
+
+ # If this is not the first plot, link to the axis of the previous one.
if self._plots:
view.setXLink(self._plots[-1].view)
- self.layout.addItem(view, row, 1)
yaxis = pyqtgraph.AxisItem(parent=self, orientation='left')
yaxis.linkToView(view)
yaxis.setGrid(255)
- self.layout.addItem(yaxis, row, 0, QtCore.Qt.AlignRight)
xaxis = pyqtgraph.AxisItem(parent=self, orientation='bottom')
xaxis.linkToView(view)
xaxis.setGrid(255)
- self.layout.addItem(xaxis, row + 1, 1)
+
+ plot = Plot(view, xaxis, yaxis)
+ self._plots.append(plot)
+
+ self.showPlot(plot)
+
+ # Create a separate action object for each plots context menu, so that
+ # we can later find out which plot should be hidden by looking at
+ # 'self._hideActions'.
+ hideAction = QtGui.QAction('Hide', self)
+ hideAction.triggered.connect(self._onHideActionTriggered)
+ self._hideActions[id(hideAction)] = plot
+ view.menu.insertAction(view.menu.actions()[0], hideAction)
+
+ return plot
+
+ def _rowNumber(self, plot):
+ '''Returns the number of the first row a plot occupies.'''
+
+ # Every plot takes up two rows
+ return 2 * self._plots.index(plot)
+
+ @QtCore.Slot()
+ def _onHideActionTriggered(self, checked=False):
+ # The plot that we want to hide.
+ plot = self._hideActions[id(self.sender())]
+ self.hidePlot(plot)
+
+ def hidePlot(self, plot):
+ '''Hides 'plot'.'''
+
+ # Only hiding wouldn't give up the space occupied by the items,
+ # we have to remove them from the layout.
+ self.layout().removeItem(plot.view)
+ self.layout().removeItem(plot.xaxis)
+ self.layout().removeItem(plot.yaxis)
+
+ plot.view.hide()
+ plot.xaxis.hide()
+ plot.yaxis.hide()
+
+ row = self._rowNumber(plot)
+ self.layout().setRowStretchFactor(row, 0)
+ self.layout().setRowStretchFactor(row + 1, 0)
+
+ plot.visible = False
+ self.plotHidden.emit(plot)
+
+ def showPlot(self, plot):
+ '''Adds the items of the plot to the scene's layout and makes
+ them visible.'''
+
+ if plot.visible:
+ return
+
+ row = self._rowNumber(plot)
+ self.layout().addItem(plot.yaxis, row, 0, QtCore.Qt.AlignRight)
+ self.layout().addItem(plot.view, row, 1)
+ self.layout().addItem(plot.xaxis, row + 1, 1)
+
+ plot.view.show()
+ plot.xaxis.show()
+ plot.yaxis.show()
for i in range(row, row + 2):
- self.layout.setRowPreferredHeight(i, 0)
- self.layout.setRowMinimumHeight(i, 0)
- self.layout.setRowSpacing(i, 0)
+ self.layout().setRowPreferredHeight(i, 0)
+ self.layout().setRowMinimumHeight(i, 0)
+ self.layout().setRowSpacing(i, 0)
- self.layout.setRowStretchFactor(row, 100)
- self.layout.setRowStretchFactor(row + 1, 0)
+ self.layout().setRowStretchFactor(row, 100)
+ self.layout().setRowStretchFactor(row + 1, 0)
- p = MultiPlotItem.Plot(view, xaxis, yaxis)
- self._plots.append(p)
- return p
+ plot.visible = True
+ self.plotShown.emit()
class MultiPlotWidget(pyqtgraph.GraphicsView):
'''Widget that aligns multiple plots on top of each other.
self.setCentralItem(self.multiPlotItem)
for m in [
- 'addPlot'
+ 'addPlot',
+ 'showPlot'
]:
setattr(self, m, getattr(self.multiPlotItem, m))
+
+ self.multiPlotItem.plotShown.connect(self._on_plotShown)
+
+ # Expose the signal of the plot item.
+ self.plotHidden = self.multiPlotItem.plotHidden
+
+ def _on_plotShown(self):
+ # This call is needed if only one plot exists and it was hidden,
+ # without it the layout would start acting weird and not make the
+ # MultiPlotItem fill the view widget after showing the plot again.
+ self.resizeEvent(None)