From 9649e1837a85030ea5189b04a1433f4f451a9233 Mon Sep 17 00:00:00 2001 From: Sebastien Bourdeauducq Date: Thu, 16 Jul 2015 20:52:53 +0200 Subject: [PATCH] gui: basic plotting --- artiq/frontend/artiq_gui.py | 4 +- artiq/gui/displays.py | 130 ++++++++++++++++++++++++++++++++++++ artiq/gui/results.py | 59 ++++++++++++++-- 3 files changed, 185 insertions(+), 8 deletions(-) create mode 100644 artiq/gui/displays.py diff --git a/artiq/frontend/artiq_gui.py b/artiq/frontend/artiq_gui.py index 9169fb531..2db26042b 100755 --- a/artiq/frontend/artiq_gui.py +++ b/artiq/frontend/artiq_gui.py @@ -3,12 +3,12 @@ import argparse import asyncio import atexit +import os # Quamash must be imported first so that pyqtgraph picks up the Qt binding # it has chosen. from quamash import QEventLoop, QtGui from pyqtgraph import dockarea -import os from artiq.protocols.file_db import FlatFileDB from artiq.protocols.pc_rpc import AsyncioClient @@ -71,7 +71,7 @@ def main(): args.server, args.port_notify)) atexit.register(lambda: loop.run_until_complete(d_explorer.sub_close())) - d_results = ResultsDock() + d_results = ResultsDock(win, area) loop.run_until_complete(d_results.sub_connect( args.server, args.port_notify)) atexit.register(lambda: loop.run_until_complete(d_results.sub_close())) diff --git a/artiq/gui/displays.py b/artiq/gui/displays.py new file mode 100644 index 000000000..a29e42526 --- /dev/null +++ b/artiq/gui/displays.py @@ -0,0 +1,130 @@ +from collections import OrderedDict + +from quamash import QtGui +import pyqtgraph as pg +from pyqtgraph import dockarea + + +class _SimpleSettings(QtGui.QDialog): + def __init__(self, parent, prev_name, prev_settings, + result_list, create_cb): + QtGui.QDialog.__init__(self, parent=parent) + self.setWindowTitle(self._window_title) + + grid = QtGui.QGridLayout() + self.setLayout(grid) + + grid.addWidget(QtGui.QLabel("Name:"), 0, 0) + self.name = name = QtGui.QLineEdit() + grid.addWidget(name, 0, 1) + if prev_name is not None: + name.insert(prev_name) + + grid.addWidget(QtGui.QLabel("Result:")) + self.result = result = QtGui.QComboBox() + grid.addWidget(result, 1, 1) + result.addItems(result_list) + result.setEditable(True) + if "result" in prev_settings: + result.setEditText(prev_settings["result"]) + + buttons = QtGui.QDialogButtonBox( + QtGui.QDialogButtonBox.Ok | QtGui.QDialogButtonBox.Cancel) + grid.addWidget(buttons, 2, 0, 1, 2) + buttons.accepted.connect(self.accept) + buttons.rejected.connect(self.reject) + + def on_accept(): + create_cb(name.text(), {"result": result.currentText()}) + self.accepted.connect(on_accept) + + def accept(self): + if self.name.text() and self.result.currentText(): + QtGui.QDialog.accept(self) + + +class NumberDisplaySettings(_SimpleSettings): + _window_title = "Number display" + + +class NumberDisplay(dockarea.Dock): + def __init__(self, name, settings): + dockarea.Dock.__init__(self, "Display: " + name, size=(250, 250), + closable=True) + self.settings = settings + self.number = QtGui.QLCDNumber() + self.number.setDigitCount(10) + self.addWidget(self.number) + + def data_sources(self): + return {self.settings["result"]} + + def update_data(self, data): + result = self.settings["result"] + try: + n = float(data[result]) + except: + n = 0.0 + self.number.display(n) + + +class XYDisplaySettings(_SimpleSettings): + _window_title = "XY plot" + + +class XYDisplay(dockarea.Dock): + def __init__(self, name, settings): + dockarea.Dock.__init__(self, "XY: " + name, size=(640, 480), + closable=True) + self.settings = settings + self.plot = pg.PlotWidget() + self.addWidget(self.plot) + + def data_sources(self): + return {self.settings["result"]} + + def update_data(self, data): + result = self.settings["result"] + try: + y = data[result] + except KeyError: + return + self.plot.clear() + if not y: + return + self.plot.plot(y) + + +class HistogramDisplaySettings(_SimpleSettings): + _window_title = "Histogram" + + +class HistogramDisplay(dockarea.Dock): + def __init__(self, name, settings): + dockarea.Dock.__init__(self, "Histogram: " + name, size=(640, 480), + closable=True) + self.settings = settings + self.plot = pg.PlotWidget() + self.addWidget(self.plot) + + def data_sources(self): + return {self.settings["result"]} + + def update_data(self, data): + result = self.settings["result"] + try: + y = data[result] + except KeyError: + return + x = list(range(len(y)+1)) + self.plot.clear() + if not y: + return + self.plot.plot(x, y, stepMode=True, fillLevel=0, brush=(0, 0, 255, 150)) + + +display_types = OrderedDict([ + ("Number", (NumberDisplaySettings, NumberDisplay)), + ("XY", (XYDisplaySettings, XYDisplay)), + ("Histogram", (HistogramDisplaySettings, HistogramDisplay)) +]) diff --git a/artiq/gui/results.py b/artiq/gui/results.py index 316fd6cac..40522d719 100644 --- a/artiq/gui/results.py +++ b/artiq/gui/results.py @@ -1,4 +1,6 @@ import asyncio +from collections import OrderedDict +from functools import partial from quamash import QtGui, QtCore from pyqtgraph import dockarea @@ -6,6 +8,7 @@ from pyqtgraph import LayoutWidget from artiq.protocols.sync_struct import Subscriber from artiq.gui.tools import DictSyncModel +from artiq.gui.displays import * def _fmt_type(v): @@ -34,8 +37,10 @@ class ResultsModel(DictSyncModel): class ResultsDock(dockarea.Dock): - def __init__(self): + def __init__(self, dialog_parent, dock_area): dockarea.Dock.__init__(self, "Results", size=(1500, 500)) + self.dialog_parent = dialog_parent + self.dock_area = dock_area grid = LayoutWidget() self.addWidget(grid) @@ -49,13 +54,17 @@ class ResultsDock(dockarea.Dock): display_grid = QtGui.QGridLayout() add_display_box.setLayout(display_grid) - for n, name in enumerate(["Number", "XY", "Histogram"]): + for n, name in enumerate(display_types.keys()): btn = QtGui.QPushButton(name) display_grid.addWidget(btn, n, 0) + btn.clicked.connect(partial(self.create_dialog, name)) + + self.displays = dict() @asyncio.coroutine def sub_connect(self, host, port): - self.subscriber = Subscriber("rt_results", self.init_results_model) + self.subscriber = Subscriber("rt_results", self.init_results_model, + self.on_mod) yield from self.subscriber.connect(host, port) @asyncio.coroutine @@ -63,6 +72,44 @@ class ResultsDock(dockarea.Dock): yield from self.subscriber.close() def init_results_model(self, init): - table_model = ResultsModel(self.table, init) - self.table.setModel(table_model) - return table_model + self.table_model = ResultsModel(self.table, init) + self.table.setModel(self.table_model) + return self.table_model + + def on_mod(self, mod): + if mod["action"] == "init": + for display in self.displays.values(): + display.update_data(self.table_model.backing_store) + return + + if mod["action"] == "setitem": + source = mod["key"] + elif mod["path"]: + source = mod["path"][0] + else: + return + + for display in self.displays.values(): + if source in display.data_sources(): + display.update_data(self.table_model.backing_store) + + def create_dialog(self, ty): + dlg_class = display_types[ty][0] + dlg = dlg_class(self.dialog_parent, None, dict(), + sorted(self.table_model.backing_store.keys()), + partial(self.create_display, ty, None)) + dlg.open() + + def create_display(self, ty, prev_name, name, settings): + if prev_name is not None and prev_name in self.displays: + raise NotImplementedError + dsp_class = display_types[ty][1] + dsp = dsp_class(name, settings) + self.displays[name] = dsp + dsp.update_data(self.table_model.backing_store) + + def on_close(): + del self.displays[name] + dsp.sigClosed.connect(on_close) + self.dock_area.addDock(dsp) + self.dock_area.floatDock(dsp)