diff --git a/README.md b/README.md index e5b5c12..e64dff6 100644 --- a/README.md +++ b/README.md @@ -22,11 +22,11 @@ Via the RPC server, new plots can be created and new data appended to existing p - `new_plot(name, cfg)` -Creates a new plot named `name` in the grum list. The configuration dict `cfg` is used as arguments for the constructor of [`PlotDescription`](https://gitlab.psi.ch/augustin_s/grum/-/blob/master/grum/plotdesc.py#L4). +Creates a new plot named `name` in the grum list. The configuration dict `cfg` is used as arguments for the constructor of [`PlotDescription`](https://gitlab.psi.ch/augustin_s/grum/-/blob/master/grum/descs/plotdesc.py#L4). - `append_data(name, point)` -Append data point to the plot named `name`. The new `point` is forwarded to [`PlotDescription.append()`](https://gitlab.psi.ch/augustin_s/grum/-/blob/master/grum/plotdesc.py#L18). +Append data point to the plot named `name`. The new `point` is forwarded to [`PlotDescription.append()`](https://gitlab.psi.ch/augustin_s/grum/-/blob/master/grum/descs/plotdesc.py#L18). ### Utility functions diff --git a/grum/descs/__init__.py b/grum/descs/__init__.py new file mode 100644 index 0000000..d641fb3 --- /dev/null +++ b/grum/descs/__init__.py @@ -0,0 +1,13 @@ + +from .desc import Description +from .imgdesc import ImageDescription +from .plotdesc import PlotDescription + + +DESC_TYPES = { + ImageDescription.get_type(): ImageDescription, + PlotDescription.get_type(): PlotDescription +} + + + diff --git a/grum/descs/desc.py b/grum/descs/desc.py new file mode 100644 index 0000000..8a85046 --- /dev/null +++ b/grum/descs/desc.py @@ -0,0 +1,23 @@ + +class Description: + + def to_dict(self): + res = {k: v for k, v in self.__dict__.items() if not k.startswith("_") and k != "name" and v is not None} + tn = self.get_type() + res.setdefault("type", tn) + return res + + + @classmethod + def get_type(cls): + tn = cls.__name__ + suffix = "Description" + if not tn.endswith(suffix): + raise ValueError(f'"{tn}" does not end with "{suffix}"') + tn = tn[:-len(suffix)] + tn = tn.casefold() + tn = tn or None + return tn + + + diff --git a/grum/descs/imgdesc.py b/grum/descs/imgdesc.py new file mode 100644 index 0000000..0d29e75 --- /dev/null +++ b/grum/descs/imgdesc.py @@ -0,0 +1,52 @@ +import numpy as np +import pyqtgraph as pg +from .desc import Description + + +class ImageDescription(Description): + + def __init__(self, name, title=None, xlabel=None, ylabel=None, image=None, levels=None, cmap="viridis"): + self.name = name + self.title = title + self.xlabel = xlabel + self.ylabel = ylabel + self.image = image + self.levels = levels #TODO: might be better to use vmin and vmax + self.cmap = cmap + + @property + def data(self): + return np.asarray(self.image) + + @data.setter + def data(self, value): + self.image = value + + + def append(self, xy): + print("ignored image append") + + def extend(self, data): + print("ignored image extend") + + + def make_plot(self, plotwidget, style): + res = plotwidget.setImage(self.data, levels=self.levels) + + if self.title: + plotwidget.setTitle(self.title) + + if self.xlabel: + plotwidget.getView().setLabel("bottom", self.xlabel) + + if self.ylabel: + plotwidget.getView().setLabel("left", self.ylabel) + + if self.cmap: + cm = pg.colormap.get(self.cmap) + plotwidget.setColorMap(cm) + + return res + + + diff --git a/grum/plotdesc.py b/grum/descs/plotdesc.py similarity index 86% rename from grum/plotdesc.py rename to grum/descs/plotdesc.py index 45832f3..86a399d 100644 --- a/grum/plotdesc.py +++ b/grum/descs/plotdesc.py @@ -1,5 +1,7 @@ +from .desc import Description -class PlotDescription: + +class PlotDescription(Description): def __init__(self, name, title=None, xlabel=None, ylabel=None, xs=None, ys=None): self.name = name @@ -45,8 +47,4 @@ class PlotDescription: return res - def to_dict(self): - return {k: v for k, v in self.__dict__.items() if not k.startswith("_") and k != "name" and v is not None} - - diff --git a/grum/exampledata.py b/grum/exampledata.py index 8eb609c..2a59d7f 100644 --- a/grum/exampledata.py +++ b/grum/exampledata.py @@ -1,6 +1,6 @@ import numpy as np -from .plotdesc import PlotDescription +from .descs import PlotDescription, ImageDescription X = np.arange(100) / 10 @@ -31,4 +31,20 @@ for name, (xs, ys) in exampledata_raw.items(): ) +name = "image" +xdim = ydim = 100 +size = xdim * ydim +shape = (xdim, ydim) + +img = np.arange(size).reshape(shape) / size +img += np.random.random(shape) / 10 + +exampledata[name] = ImageDescription( + name, + image=img, + xlabel="x", + ylabel="y" +) + + diff --git a/grum/mainwin.py b/grum/mainwin.py index a2e4fd9..a37de16 100644 --- a/grum/mainwin.py +++ b/grum/mainwin.py @@ -2,21 +2,27 @@ from PyQt5.QtCore import Qt, pyqtSignal from PyQt5.QtWidgets import QMainWindow, QSplitter from . import assets +from .descs import DESC_TYPES, Description, ImageDescription, PlotDescription from .dictlist import DictList from .exampledata import exampledata from .h5filedlg import open_h5_files_dialog, save_h5_file_dialog from .io import write_dict, read_dict -from .mdi import MDIArea, MDISubMultiPlot, MDISubPlot, MDIWindowMode +from .mdi import MDIArea, MDISubMultiPlot, MDISubPlot, MDISubImage, MDIWindowMode from .menus import BarMenu -from .plotdesc import PlotDescription from .rpc import RPCServerThread from .shortcut import shortcut from .webview import WebView +DESC_TYPE_TO_MDI_SUB_TYPE = { + ImageDescription: MDISubImage, + PlotDescription: MDISubPlot +} + + class MainWindow(QMainWindow): - sig_make_new_plot = pyqtSignal(str, PlotDescription) + sig_make_new_subwin = pyqtSignal(str, Description) def __init__(self, *args, title="grum", host="localhost", port=8000, offline=False, add_examples=False, window_mode=MDIWindowMode.MULTI, **kwargs): super().__init__(*args, **kwargs) @@ -84,12 +90,13 @@ class MainWindow(QMainWindow): if not offline: self.rst = rst = RPCServerThread(host, port, doc_title_suffix=title) rst.start() + rst.server.register_function(self.new_image) rst.server.register_function(self.new_plot) rst.server.register_function(self.append_data) rst.server.register_function(self.extend_data) rst.server.register_function(self.set_data) - self.sig_make_new_plot.connect(self.on_make_new_plot) + self.sig_make_new_subwin.connect(self.on_make_new_subwin) def keyPressEvent(self, event): @@ -99,16 +106,30 @@ class MainWindow(QMainWindow): # Remote API calls + def new_image(self, name, cfg): + """ + Create a new image using the configuration dict . + The configuration is forwarded to the constructor of ImageDescription. + Allowed keys are: title, xlabel, ylabel, image. + """ + desc = self.add_new_desc_to_list(ImageDescription, name, cfg) + if self.menu_settings.checkboxes["Open new plots"].isChecked(): + sub = self.mdi.findSubWindow(name) + if sub: + sub.pw.setImage(desc.data) #TODO lacks the list sync + else: + self.sig_make_new_subwin.emit(name, desc) + def new_plot(self, name, cfg): """ Create a new plot using the configuration dict . The configuration is forwarded to the constructor of PlotDescription. Allowed keys are: title, xlabel, ylabel, xs, ys. """ - desc = self.add_new_desc_to_list(name, cfg) + desc = self.add_new_desc_to_list(PlotDescription, name, cfg) if self.menu_settings.checkboxes["Open new plots"].isChecked(): if not self.mdi.findSubWindow(name): - self.sig_make_new_plot.emit(name, desc) + self.sig_make_new_subwin.emit(name, desc) def append_data(self, name, point): """ @@ -143,8 +164,10 @@ class MainWindow(QMainWindow): # Signal callbacks - def on_make_new_plot(self, *args, **kwargs): - self.make_subwin(MDISubPlot, *args, **kwargs) + def on_make_new_subwin(self, name, desc): + DescType = type(desc) + MDISubType = DESC_TYPE_TO_MDI_SUB_TYPE[DescType] + self.make_subwin(MDISubType, name, desc) def on_dclick_list_item(self, item): self.plot_single_item(item) @@ -187,8 +210,10 @@ class MainWindow(QMainWindow): for fn in fns: data = read_dict(fn) - for k, v in data.items(): - self.add_new_desc_to_list(k, v) + for name, cfg in data.items(): + tn = cfg.pop("type") + DescType = DESC_TYPES[tn] + self.add_new_desc_to_list(DescType, name, cfg) def on_file_save(self): @@ -206,8 +231,8 @@ class MainWindow(QMainWindow): # Plumbing - def add_new_desc_to_list(self, name, cfg): - desc = PlotDescription(name, **cfg) + def add_new_desc_to_list(self, DescType, name, cfg): + desc = DescType(name, **cfg) self.lst.set(name, desc) return desc @@ -226,19 +251,22 @@ class MainWindow(QMainWindow): item.timestamps.access.update() item.set_alarm(False) name, desc = item.key, item.value - self.activate_or_make_subwin(MDISubPlot, name, desc) + DescType = type(desc) + MDISubType = DESC_TYPE_TO_MDI_SUB_TYPE[DescType] + self.activate_or_make_subwin(MDISubType, name, desc) def plot_multiple_items(self, items): for i in items: i.timestamps.access.update() i.set_alarm(False) + items = (i for i in items if isinstance(i.value, PlotDescription)) #TODO: for now, only overlay plots descs = {i.key: i.value for i in items} names = descs.keys() name = " | ".join(names) self.activate_or_make_subwin(MDISubMultiPlot, name, descs) - #TODO: the following two could be methods to MDIArea? + #TODO: the following two could be methods of MDIArea? def activate_or_make_subwin(self, MDISubType, name, *args, **kwargs): sub = self.mdi.findSubWindow(name) diff --git a/grum/mdi/__init__.py b/grum/mdi/__init__.py index 4820247..e754999 100644 --- a/grum/mdi/__init__.py +++ b/grum/mdi/__init__.py @@ -1,5 +1,6 @@ from .mdiarea import MDIArea, MDIWindowMode from .mdisubplot import MDISubPlot, MDISubMultiPlot +from .mdisubimg import MDISubImage diff --git a/grum/mdi/mdisubimg.py b/grum/mdi/mdisubimg.py new file mode 100644 index 0000000..9f78974 --- /dev/null +++ b/grum/mdi/mdisubimg.py @@ -0,0 +1,38 @@ +import pyqtgraph as pg +from .mdisubwin import MDISubWindow +from ..theme import pg_plot_style + + +class MDISubImage(MDISubWindow): + + def __init__(self, name, desc, *args, **kwargs): + super().__init__(name, *args, **kwargs) + self.pw = pw = pg.ImageView(view=pg.PlotItem()) # for axis ticks and labels, view needs to be a PlotItem + self.setWidget(pw) + + # connect to plot mouse-over event + pw.scene.sigMouseMoved.connect(self.on_hover) + + style = pg_plot_style() + + plot = desc.make_plot(self.pw, style) + self.plots = {name: plot} + + self.image = desc.data + + + def on_hover(self, event): + coord = self.pw.imageItem.mapFromScene(event) + x = coord.x() + y = coord.y() + x = int(x) + y = int(y) + try: + z = self.image[x, y] + except IndexError: + return + z = round(z, 3) + self.setToolTip(f"x = {x}\ny = {y}\nz = {z}") + + + diff --git a/grum/mdi/mdisubwin.py b/grum/mdi/mdisubwin.py index 64c45a1..03c5518 100644 --- a/grum/mdi/mdisubwin.py +++ b/grum/mdi/mdisubwin.py @@ -4,12 +4,17 @@ from PyQt5.QtWidgets import QMdiSubWindow from .. import assets +SUB_WIN_WIDTH = 640 +SUB_WIN_HEIGHT = 480 + + class MDISubWindow(QMdiSubWindow): def __init__(self, title, *args, **kwargs): super().__init__(*args, **kwargs) self.setWindowTitle(title) self.setWindowIcon(assets.char()) + self.resize(SUB_WIN_WIDTH, SUB_WIN_HEIGHT) # without this, the SubWindow is not removed from the subWindowList self.setAttribute(Qt.WA_DeleteOnClose) diff --git a/tests/test_mainwin.py b/tests/test_mainwin.py index 6a73af4..53751d9 100644 --- a/tests/test_mainwin.py +++ b/tests/test_mainwin.py @@ -14,7 +14,7 @@ from grum.mainwin import MainWindow from grum.mdi import MDIArea, MDISubMultiPlot, MDISubPlot from grum.menus import BarMenu from grum.menus.rclickmenu import RClickMenu -from grum.plotdesc import PlotDescription +from grum.descs import Description, PlotDescription from grum.rpc import RPCServerThread @@ -47,7 +47,7 @@ class TestMainWin: for key in mw.lst.lst.items: assert isinstance(mw.lst.lst.get(key), DictListItem) - assert isinstance(mw.lst.lst.get(key).value, PlotDescription) + assert isinstance(mw.lst.lst.get(key).value, Description) assert isinstance(mw.lst.menu, RClickMenu) assert isinstance(mw.menu_settings, BarMenu) @@ -65,7 +65,7 @@ class TestMainWin: xlabel = "xlabel" ylabel = "ylabel" cfg = {"title": title, "xlabel": xlabel, "ylabel": ylabel} - spy_sig_make_new_plot = QSignalSpy(mw.sig_make_new_plot) + spy_sig_make_new_subwin = QSignalSpy(mw.sig_make_new_subwin) mw.new_plot(name, cfg=cfg) @@ -76,28 +76,28 @@ class TestMainWin: assert mw.lst.lst.get(name).value.ylabel == ylabel assert mw.menu_settings.checkboxes["Open new plots"].isChecked() - assert len(spy_sig_make_new_plot) == 1 # assert called once - assert spy_sig_make_new_plot[0][0] == name # assert called with name - assert isinstance(spy_sig_make_new_plot[0][1], PlotDescription) + assert len(spy_sig_make_new_subwin) == 1 # assert called once + assert spy_sig_make_new_subwin[0][0] == name # assert called with name + assert isinstance(spy_sig_make_new_subwin[0][1], PlotDescription) mw.menu_settings.checkboxes["Open new plots"].setChecked(False) assert mw.menu_settings.checkboxes["Open new plots"].isChecked() == False - spy_sig_make_new_plot = QSignalSpy(mw.sig_make_new_plot) + spy_sig_make_new_subwin = QSignalSpy(mw.sig_make_new_subwin) mw.new_plot("new_name", cfg) - assert len(spy_sig_make_new_plot) == 0 # assert not called + assert len(spy_sig_make_new_subwin) == 0 # assert not called mw.menu_settings.checkboxes["Open new plots"].setChecked(True) assert mw.menu_settings.checkboxes["Open new plots"].isChecked() == True - spy_sig_make_new_plot = QSignalSpy(mw.sig_make_new_plot) + spy_sig_make_new_subwin = QSignalSpy(mw.sig_make_new_subwin) new_name_item = mw.lst.lst.get("new_name") sub = MDISubPlot("new_name", new_name_item.value) mw.mdi.add(sub) mw.new_plot("new_name", cfg) - assert len(spy_sig_make_new_plot) == 0 # assert not called + assert len(spy_sig_make_new_subwin) == 0 # assert not called def test_append_data(self): @@ -122,17 +122,19 @@ class TestMainWin: assert sine_item.set_alarm.call_args[0][0] == False - def test_on_make_new_plot(self): + def test_on_make_new_subwin(self): mw = self.mw mw.make_subwin = mock.MagicMock() - args = (1, 2, "name") - kwargs = {"title": "plot_title"} + name = "test" + cfg = {"title": "title"} - mw.on_make_new_plot(args, kwargs) + desc = PlotDescription(name, *cfg) - mw.make_subwin.assert_called_once_with(MDISubPlot, args, kwargs) + mw.on_make_new_subwin(name, desc) + + mw.make_subwin.assert_called_once_with(MDISubPlot, name, desc) def test_on_dclick_list_item(self): diff --git a/tests/test_plotdesc.py b/tests/test_plotdesc.py index 06fd68c..0d46110 100644 --- a/tests/test_plotdesc.py +++ b/tests/test_plotdesc.py @@ -6,7 +6,7 @@ import pyqtgraph as pg from grum import theme from grum.mainwin import MainWindow from grum.mdi.mdisubplot import MDISubPlot -from grum.plotdesc import PlotDescription +from grum.descs import PlotDescription from grum.theme import pg_plot_style @@ -78,6 +78,7 @@ def test_to_dict(): "xs": [1, 2], "ylabel": "plot_ylabel", "ys": [3, 4], + "type": "plot" }