From: Jens Steinhauser Date: Sun, 4 Oct 2015 03:20:44 +0000 (+0200) Subject: Make it possible to hide plots. X-Git-Url: http://sigrok.org/gitweb/?p=sigrok-meter.git;a=commitdiff_plain;h=d0aa45b43873337a187f274d5c2919e994941f7f Make it possible to hide plots. --- diff --git a/datamodel.py b/datamodel.py index 52884eb..53c70bc 100644 --- a/datamodel.py +++ b/datamodel.py @@ -33,6 +33,17 @@ except ImportError: 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.''' @@ -42,8 +53,8 @@ class MeasurementDataModel(QtGui.QStandardItemModel): '''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 @@ -126,7 +137,7 @@ class MeasurementDataModel(QtGui.QStandardItemModel): 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) @@ -151,8 +162,8 @@ class MeasurementDataModel(QtGui.QStandardItemModel): # 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.''' diff --git a/mainwindow.py b/mainwindow.py index 19ce4d3..8ac3b3d 100644 --- a/mainwindow.py +++ b/mainwindow.py @@ -114,6 +114,7 @@ class MainWindow(QtGui.QMainWindow): 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 = {} @@ -174,33 +175,57 @@ class MainWindow(QtGui.QMainWindow): 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() diff --git a/multiplotwidget.py b/multiplotwidget.py index 69e871a..1d257bd 100755 --- a/multiplotwidget.py +++ b/multiplotwidget.py @@ -28,62 +28,136 @@ pyqtgraph = qtcompat.pyqtgraph 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. @@ -98,6 +172,18 @@ class MultiPlotWidget(pyqtgraph.GraphicsView): 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)