From 6caca561b71434221422cc7fb6704c1c52dc3cc5 Mon Sep 17 00:00:00 2001 From: Valentin Valls Date: Sat, 31 Oct 2020 17:54:05 +0100 Subject: [PATCH 01/18] Move custom plot to Flint window --- bliss/flint/flint_api.py | 47 ++++++++++++++----------------------- bliss/flint/flint_window.py | 38 ++++++++++++++++++++++++++---- 2 files changed, 52 insertions(+), 33 deletions(-) diff --git a/bliss/flint/flint_api.py b/bliss/flint/flint_api.py index a9c589092a..56e6254cae 100755 --- a/bliss/flint/flint_api.py +++ b/bliss/flint/flint_api.py @@ -42,14 +42,6 @@ from bliss.flint import config _logger = logging.getLogger(__name__) -class CustomPlot(NamedTuple): - """Store information to a plot created remotly and providing silx API.""" - - plot: qt.QWidget - tab: qt.QWidget - title: str - - class Request(NamedTuple): """Store information about a request.""" @@ -108,8 +100,6 @@ class FlintApi: """Store the current requests""" self.__flintModel = flintModel - # FIXME: _custom_plots should be owned by flint model or window - self._custom_plots: Dict[object, CustomPlot] = {} self.data_event = collections.defaultdict(dict) self.data_dict = collections.defaultdict(dict) @@ -413,7 +403,8 @@ class FlintApi: except ValueError: return False else: - return plot_id in self._custom_plots + window = self.__flintModel.mainWindow() + return window.customPlot(plot_id) is not None def add_plot( self, @@ -442,30 +433,24 @@ class FlintApi: plot_id = self.create_new_id() if not name: name = "Plot %d" % plot_id - new_tab_widget = self.__flintModel.mainWindow().createTab( - name, selected=selected, closeable=closeable - ) - # FIXME: Hack to know how to close the widget - new_tab_widget._plot_id = plot_id - qt.QVBoxLayout(new_tab_widget) + window = self.__flintModel.mainWindow() cls = getattr(silx_plot, cls_name) - plot = cls(new_tab_widget) - self._custom_plots[plot_id] = CustomPlot(plot, new_tab_widget, name) - new_tab_widget.layout().addWidget(plot) - plot.show() + plot = cls(window) + window.createCustomPlot( + plot, name, plot_id, selected=selected, closeable=closeable + ) return plot_id def get_plot_name(self, plot_id): if isinstance(plot_id, str) and plot_id.startswith("live:"): widget = self._get_live_plot_widget(plot_id) return widget.windowTitle() - return self._custom_plots[plot_id].title + window = self.__flintModel.mainWindow() + return window.customPlot(plot_id).title def remove_plot(self, plot_id): - custom_plot = self._custom_plots.pop(plot_id) window = self.__flintModel.mainWindow() - window.removeTab(custom_plot.tab) - custom_plot.plot.close() + return window.removeCustomPlot(plot_id) def get_interface(self, plot_id): plot = self._get_plot_widget(plot_id) @@ -711,7 +696,9 @@ class FlintApi: if isinstance(plot_id, str) and plot_id.startswith("live:"): widget = self._get_live_plot_widget(plot_id) return widget - return self._custom_plots[plot_id].tab + window = self.__flintModel.mainWindow() + customPlot = window.customPlot(plot_id) + return customPlot.tab def _get_plot_widget(self, plot_id, expect_silx_api=True, custom_plot=False): # FIXME: Refactor it, it starts to be ugly @@ -730,9 +717,11 @@ class FlintApi: f"The widget associated to '{plot_id}' only provides a silx API" ) + window = self.__flintModel.mainWindow() + customPlot = window.customPlot(plot_id) if custom_plot: - return self._custom_plots[plot_id] - return self._custom_plots[plot_id].plot + return customPlot + return customPlot.plot # API to custom default live plots @@ -869,7 +858,7 @@ class FlintApi: custom_plot = self._get_plot_widget(plot_id, custom_plot=True) # Set the focus as an user input is requested - if isinstance(custom_plot, CustomPlot): + if isinstance(custom_plot, NamedTuple): plot = custom_plot.plot # Set the focus as an user input is requested window = self.__flintModel.mainWindow() diff --git a/bliss/flint/flint_window.py b/bliss/flint/flint_window.py index 7c290de422..4ae26f2cc3 100755 --- a/bliss/flint/flint_window.py +++ b/bliss/flint/flint_window.py @@ -6,6 +6,10 @@ # Distributed under the GNU LGPLv3. See LICENSE for more info. """Module containing the description of the main window provided by Flint""" +from __future__ import annotations +from typing import Dict +from typing import NamedTuple + import logging import os @@ -19,6 +23,14 @@ from bliss.flint.model import flint_model _logger = logging.getLogger(__name__) +class CustomPlot(NamedTuple): + """Store information to a plot created remotly and providing silx API.""" + + plot: qt.QWidget + tab: qt.QWidget + title: str + + class FlintWindow(qt.QMainWindow): """"Main Flint window""" @@ -28,6 +40,7 @@ class FlintWindow(qt.QMainWindow): self.__flintState: flint_model.FlintState = None self.__stateIndicator: StateIndicator = None + self.__customPlots: Dict[object, CustomPlot] = {} central_widget = qt.QWidget(self) @@ -59,11 +72,8 @@ class FlintWindow(qt.QMainWindow): def __tabCloseRequested(self, tabIndex): new_tab_widget = self.__tabs.widget(tabIndex) - # FIXME: CustomPlot should not be a flint_api concept - # FIXME: There should not be a link to flint_api plot_id = new_tab_widget._plot_id - flintApi = self.__flintState.flintApi() - flintApi.remove_plot(plot_id) + self.removeCustomPlot(plot_id) def __initLogWindow(self): logWindow = qt.QDialog(self) @@ -293,3 +303,23 @@ class FlintWindow(qt.QMainWindow): settings.setValue("size", self.__logWindow.size()) settings.setValue("pos", self.__logWindow.pos()) settings.endGroup() + + def createCustomPlot(self, widget, name, plot_id, selected, closeable): + """Create a custom plot""" + widgetHolder = self.createTab(name, selected=selected, closeable=closeable) + # FIXME: Hack to know how to close the widget + widgetHolder._plot_id = plot_id + qt.QVBoxLayout(widgetHolder) + self.__customPlots[plot_id] = CustomPlot(widget, widgetHolder, name) + widgetHolder.layout().addWidget(widget) + widget.show() + + def removeCustomPlot(self, plot_id): + """Remove a custom plot by its id""" + customPlot = self.__customPlots.pop(plot_id) + self.removeTab(customPlot.tab) + customPlot.plot.close() + + def customPlot(self, plot_id) -> CustomPlot: + """If the plot does not exist, returns None""" + return self.__customPlots.get(plot_id) -- GitLab From bb359c3ddf3496bd727adf4b7cb7939b0c4a8416 Mon Sep 17 00:00:00 2001 From: Valentin Valls Date: Sat, 31 Oct 2020 19:37:59 +0100 Subject: [PATCH 02/18] Create a real class to hold custom plots --- bliss/flint/flint_api.py | 139 ++++++++++++++--------------- bliss/flint/flint_window.py | 39 ++++---- bliss/flint/widgets/custom_plot.py | 43 +++++++++ 3 files changed, 129 insertions(+), 92 deletions(-) create mode 100644 bliss/flint/widgets/custom_plot.py diff --git a/bliss/flint/flint_api.py b/bliss/flint/flint_api.py index 56e6254cae..6b300c4e0b 100755 --- a/bliss/flint/flint_api.py +++ b/bliss/flint/flint_api.py @@ -38,6 +38,7 @@ from bliss.flint.model import plot_state_model from bliss.flint.model import flint_model from bliss.common import event from bliss.flint import config +from bliss.flint.widgets.custom_plot import CustomPlot _logger = logging.getLogger(__name__) @@ -304,8 +305,9 @@ class FlintApi: manager.waitFlintStarted() def run_method(self, plot_id, method, args, kwargs): - plot = self._get_plot_widget(plot_id, expect_silx_api=True) - method = getattr(plot, method) + plot = self._get_plot_widget(plot_id) + silxPlot = plot._silxPlot() + method = getattr(silxPlot, method) return method(*args, **kwargs) def ping(self, msg=None, stderr=False): @@ -322,7 +324,7 @@ class FlintApi: def test_count_displayed_items(self, plot_id): """Debug purpose function to count number of displayed items in a plot widget.""" - widget = self._get_plot_widget(plot_id, expect_silx_api=False, custom_plot=True) + widget = self._get_plot_widget(plot_id) if widget is None: raise Exception("Widget %s not found" % plot_id) count = 0 @@ -334,7 +336,7 @@ class FlintApi: def test_displayed_channel_names(self, plot_id): """Debug purpose function to returns displayed channels from a live plot widget.""" - widget = self._get_plot_widget(plot_id, expect_silx_api=False, custom_plot=True) + widget = self._get_plot_widget(plot_id, custom_plot=False) if widget is None: raise Exception("Widget %s not found" % plot_id) plot = widget.plotModel() @@ -350,7 +352,7 @@ class FlintApi: qaction: The action which will be processed. It have to be a children of the plot and referenced as it's object name. """ - plot = self._get_plot_widget(plot_id, expect_silx_api=True) + plot = self._get_plot_widget(plot_id) action: qt.QAction = plot.findChild(qt.QAction, qaction) action.trigger() @@ -370,10 +372,11 @@ class FlintApi: position: Expected position of the mouse relative_to_center: If try the position is relative to center """ - plot = self._get_plot_widget(plot_id, expect_silx_api=True) + plot = self._get_plot_widget(plot_id) + silxPlot = plot._silxPlot() from silx.gui.utils.testutils import QTest - widget = plot.getWidgetHandle() + widget = silxPlot.getWidgetHandle() assert relative_to_center is True rect = qt.QRect(qt.QPoint(0, 0), widget.size()) base = rect.center() @@ -442,11 +445,11 @@ class FlintApi: return plot_id def get_plot_name(self, plot_id): - if isinstance(plot_id, str) and plot_id.startswith("live:"): - widget = self._get_live_plot_widget(plot_id) + widget = self._get_plot_widget(plot_id) + if isinstance(widget, CustomPlot): + return widget.name() + else: return widget.windowTitle() - window = self.__flintModel.mainWindow() - return window.customPlot(plot_id).title def remove_plot(self, plot_id): window = self.__flintModel.mainWindow() @@ -454,7 +457,8 @@ class FlintApi: def get_interface(self, plot_id): plot = self._get_plot_widget(plot_id) - names = dir(plot) + silxPlot = plot._silxPlot() + names = dir(silxPlot) # Deprecated attrs removes = ["DEFAULT_BACKEND"] for r in removes: @@ -483,7 +487,7 @@ class FlintApi: finite, the marker is removed (or not created) text: A text label for the marker """ - plot = self._get_plot_widget(plot_id, expect_silx_api=False) + plot = self._get_plot_widget(plot_id, custom_plot=False) model = plot.plotModel() if model is None: raise Exception("No model linked to this plot") @@ -523,7 +527,7 @@ class FlintApi: it is removed from the plot """ ydata = _aswritablearray(ydata) - plot = self._get_plot_widget(plot_id, expect_silx_api=False) + plot = self._get_plot_widget(plot_id, custom_plot=False) model = plot.plotModel() if model is None: raise Exception("No model linked to this plot") @@ -601,25 +605,29 @@ class FlintApi: return self.data_dict[plot_id].get(field, []) def select_data(self, plot_id, method, names, kwargs): - plot = self._get_plot_widget(plot_id) + plot = self._get_plot_widget(plot_id, live_plot=False) + silxPlot = plot._silxPlot() + # Hackish legend handling if "legend" not in kwargs and method.startswith("add"): kwargs["legend"] = " -> ".join(names) # Get the data to plot args = tuple(self.data_dict[plot_id][name] for name in names) - method = getattr(plot, method) + method = getattr(silxPlot, method) # Plot method(*args, **kwargs) def deselect_data(self, plot_id, names): - plot = self._get_plot_widget(plot_id) + plot = self._get_plot_widget(plot_id, live_plot=False) + silxPlot = plot._silxPlot() legend = " -> ".join(names) - plot.remove(legend) + silxPlot.remove(legend) def clear_data(self, plot_id): self.data_dict[plot_id].clear() - plot = self._get_plot_widget(plot_id) - plot.clear() + plot = self._get_plot_widget(plot_id, live_plot=False) + silxPlot = plot._silxPlot() + silxPlot.clear() def start_image_monitoring(self, channel_name, tango_address): """Start monitoring of an image from a Tango detector. @@ -691,37 +699,26 @@ class FlintApi: widget = widgets[iwidget] return widget - def _get_widget(self, plot_id): - # FIXME: Refactor it, it starts to be ugly - if isinstance(plot_id, str) and plot_id.startswith("live:"): - widget = self._get_live_plot_widget(plot_id) - return widget - window = self.__flintModel.mainWindow() - customPlot = window.customPlot(plot_id) - return customPlot.tab + def _get_plot_widget(self, plot_id, live_plot=None, custom_plot=None): + """Get a plot widget (widget while hold a plot) from this `plot_id` - def _get_plot_widget(self, plot_id, expect_silx_api=True, custom_plot=False): - # FIXME: Refactor it, it starts to be ugly - if isinstance(plot_id, str) and plot_id.startswith("live:"): - widget = self._get_live_plot_widget(plot_id) - if not expect_silx_api: + Arguments: + plot_id: Id of a plot + live_plot: If this filter is set, a live plot is returned only if + it's set to True + custom_plot: If this filter is set, a custom plot is returned only + if it's set to True + """ + if live_plot in [None, True]: + if isinstance(plot_id, str) and plot_id.startswith("live:"): + widget = self._get_live_plot_widget(plot_id) return widget - if not hasattr(widget, "_silxPlot"): - raise ValueError( - f"The widget associated to '{plot_id}' do not provide a silx API" - ) - return widget._silxPlot() - - if not expect_silx_api: - raise ValueError( - f"The widget associated to '{plot_id}' only provides a silx API" - ) - - window = self.__flintModel.mainWindow() - customPlot = window.customPlot(plot_id) - if custom_plot: - return customPlot - return customPlot.plot + if custom_plot in [None, True]: + window = self.__flintModel.mainWindow() + customPlot = window.customPlot(plot_id) + if customPlot is not None: + return customPlot + return None # API to custom default live plots @@ -732,7 +729,7 @@ class FlintApi: - If a channel was hidden, it become visible - If a channel is in the plot but not part of this list, it is removed """ - widget = self._get_plot_widget(plot_id, expect_silx_api=False, custom_plot=True) + widget = self._get_plot_widget(plot_id) if widget is None: raise ValueError("Widget %s not found" % plot_id) @@ -775,8 +772,9 @@ class FlintApi: reach the result. The event result is list of shapes describing the selection. """ - plot = self._get_plot_widget(plot_id, expect_silx_api=True) - selector = plot_interaction.ShapesSelector(plot) + plot = self._get_plot_widget(plot_id) + silxPlot = plot._silxPlot() + selector = plot_interaction.ShapesSelector(silxPlot) if isinstance(kinds, str): kinds = [kinds] selector.setKinds(kinds) @@ -800,8 +798,9 @@ class FlintApi: is defined by a tuple of 2 floats (x, y). If nothing is selected an empty sequence is returned. """ - plot = self._get_plot_widget(plot_id, expect_silx_api=True) - selector = plot_interaction.PointsSelector(plot) + plot = self._get_plot_widget(plot_id) + silxPlot = plot._silxPlot() + selector = plot_interaction.PointsSelector(silxPlot) selector.setNbPoints(nb) return self.__request_selector(plot_id, selector) @@ -822,8 +821,9 @@ class FlintApi: A point is defined by a tuple of 2 floats (x, y). If nothing is selected an empty sequence is returned. """ - plot = self._get_plot_widget(plot_id, expect_silx_api=True) - selector = plot_interaction.ShapeSelector(plot) + plot = self._get_plot_widget(plot_id) + silxPlot = plot._silxPlot() + selector = plot_interaction.ShapeSelector(silxPlot) selector.setShapeSelection(shape) return self.__request_selector(plot_id, selector) @@ -846,8 +846,9 @@ class FlintApi: The event is a numpy.array describing the selection """ - plot = self._get_plot_widget(plot_id, expect_silx_api=True) - selector = plot_interaction.MaskImageSelector(plot) + plot = self._get_plot_widget(plot_id) + silxPlot = plot._silxPlot() + selector = plot_interaction.MaskImageSelector(silxPlot) initial_mask = _aswritablearray(initial_mask) if initial_mask is not None: selector.setInitialMask(initial_mask, copy=False) @@ -855,18 +856,16 @@ class FlintApi: return self.__request_selector(plot_id, selector) def __request_selector(self, plot_id, selector: plot_interaction.Selector) -> str: - custom_plot = self._get_plot_widget(plot_id, custom_plot=True) + plot = self._get_plot_widget(plot_id) # Set the focus as an user input is requested - if isinstance(custom_plot, NamedTuple): - plot = custom_plot.plot + window = self.__flintModel.mainWindow() + if isinstance(plot, CustomPlot): # Set the focus as an user input is requested - window = self.__flintModel.mainWindow() - window.setFocusOnPlot(custom_plot.tab) + window.setFocusOnPlot(plot) else: window = self.__flintModel.mainWindow() window.setFocusOnLiveScan() - plot = custom_plot request_id = self.__create_request_id() request = Request(plot, request_id, selector) @@ -921,18 +920,18 @@ class FlintApi: def set_plot_focus(self, plot_id): """Set the focus on a plot""" - widget = self._get_widget(plot_id) + widget = self._get_plot_widget(plot_id) if widget is None: raise ValueError("Widget %s not found" % plot_id) model = self.__flintModel window = model.mainWindow() - if isinstance(plot_id, str) and plot_id.startswith("live:"): + if isinstance(widget, CustomPlot): + window.setFocusOnPlot(widget) + else: window.setFocusOnLiveScan() widget.show() widget.raise_() widget.setFocus(qt.Qt.OtherFocusReason) - else: - window.setFocusOnPlot(widget) def set_plot_colormap( self, @@ -948,7 +947,7 @@ class FlintApi: """ Allows to setup the default colormap of a widget. """ - widget = self._get_widget(plot_id) + widget = self._get_plot_widget(plot_id) if not hasattr(widget, "defaultColormap"): raise TypeError("Widget %s does not expose a colormap" % plot_id) @@ -976,7 +975,7 @@ class FlintApi: def export_to_logbook(self, plot_id): """Export a plot to the logbook if available""" - widget = self._get_widget(plot_id) + widget = self._get_plot_widget(plot_id, custom_plot=False) if widget is None: raise ValueError("Widget %s not found" % plot_id) if not hasattr(widget, "logbookAction"): diff --git a/bliss/flint/flint_window.py b/bliss/flint/flint_window.py index 4ae26f2cc3..d40fad6bef 100755 --- a/bliss/flint/flint_window.py +++ b/bliss/flint/flint_window.py @@ -17,20 +17,13 @@ from silx.gui import qt from bliss.flint.widgets.log_widget import LogWidget from bliss.flint.widgets.live_window import LiveWindow +from bliss.flint.widgets.custom_plot import CustomPlot from bliss.flint.widgets.state_indicator import StateIndicator from bliss.flint.model import flint_model _logger = logging.getLogger(__name__) -class CustomPlot(NamedTuple): - """Store information to a plot created remotly and providing silx API.""" - - plot: qt.QWidget - tab: qt.QWidget - title: str - - class FlintWindow(qt.QMainWindow): """"Main Flint window""" @@ -71,9 +64,10 @@ class FlintWindow(qt.QMainWindow): return self.__tabs def __tabCloseRequested(self, tabIndex): - new_tab_widget = self.__tabs.widget(tabIndex) - plot_id = new_tab_widget._plot_id - self.removeCustomPlot(plot_id) + widget = self.__tabs.widget(tabIndex) + if isinstance(widget, CustomPlot): + plotId = widget.plotId() + self.removeCustomPlot(plotId) def __initLogWindow(self): logWindow = qt.QDialog(self) @@ -304,22 +298,23 @@ class FlintWindow(qt.QMainWindow): settings.setValue("pos", self.__logWindow.pos()) settings.endGroup() - def createCustomPlot(self, widget, name, plot_id, selected, closeable): + def createCustomPlot(self, plotWidget, name, plot_id, selected, closeable): """Create a custom plot""" - widgetHolder = self.createTab(name, selected=selected, closeable=closeable) - # FIXME: Hack to know how to close the widget - widgetHolder._plot_id = plot_id - qt.QVBoxLayout(widgetHolder) - self.__customPlots[plot_id] = CustomPlot(widget, widgetHolder, name) - widgetHolder.layout().addWidget(widget) - widget.show() + customPlot = self.createTab( + name, widgetClass=CustomPlot, selected=selected, closeable=closeable + ) + customPlot.setPlotId(plot_id) + customPlot.setName(name) + customPlot.setPlot(plotWidget) + self.__customPlots[plot_id] = customPlot + plotWidget.show() def removeCustomPlot(self, plot_id): """Remove a custom plot by its id""" customPlot = self.__customPlots.pop(plot_id) - self.removeTab(customPlot.tab) - customPlot.plot.close() + self.removeTab(customPlot) def customPlot(self, plot_id) -> CustomPlot: """If the plot does not exist, returns None""" - return self.__customPlots.get(plot_id) + plot = self.__customPlots.get(plot_id) + return plot diff --git a/bliss/flint/widgets/custom_plot.py b/bliss/flint/widgets/custom_plot.py new file mode 100644 index 0000000000..45118a9b92 --- /dev/null +++ b/bliss/flint/widgets/custom_plot.py @@ -0,0 +1,43 @@ +# -*- coding: utf-8 -*- +# +# This file is part of the bliss project +# +# Copyright (c) 2015-2020 Beamline Control Unit, ESRF +# Distributed under the GNU LGPLv3. See LICENSE for more info. + +import logging + +from silx.gui import qt + + +_logger = logging.getLogger(__name__) + + +class CustomPlot(qt.QWidget): + def __init__(self, parent=None): + super(CustomPlot, self).__init__(parent=parent) + layout = qt.QVBoxLayout(self) + layout.setContentsMargins(0, 0, 0, 0) + self.__plot = None + self.__plotId = None + self.__name = None + + def setName(self, name): + self.__name = name + + def name(self): + return self.__name + + def setPlotId(self, plotId): + self.__plotId = plotId + + def plotId(self): + return self.__plotId + + def setPlot(self, plot): + layout = self.layout() + layout.addWidget(plot) + self.__plot = plot + + def _silxPlot(self): + return self.__plot -- GitLab From 868b529cea62a359de6cf7ec826f8876068c1469 Mon Sep 17 00:00:00 2001 From: Valentin Valls Date: Sat, 31 Oct 2020 20:07:54 +0100 Subject: [PATCH 03/18] Move custom plot data inside the class --- bliss/flint/flint_api.py | 50 +++++++----------------------- bliss/flint/widgets/custom_plot.py | 45 ++++++++++++++++++++++++++- 2 files changed, 56 insertions(+), 39 deletions(-) diff --git a/bliss/flint/flint_api.py b/bliss/flint/flint_api.py index 6b300c4e0b..cda7340e1c 100755 --- a/bliss/flint/flint_api.py +++ b/bliss/flint/flint_api.py @@ -20,11 +20,8 @@ import sys import logging import itertools import functools -import collections import numpy -import gevent.event - from silx.gui import qt from silx.gui import plot as silx_plot import bliss @@ -101,8 +98,6 @@ class FlintApi: """Store the current requests""" self.__flintModel = flintModel - self.data_event = collections.defaultdict(dict) - self.data_dict = collections.defaultdict(dict) self.stdout = MultiplexStreamToCallback(sys.stdout) sys.stdout = self.stdout @@ -148,14 +143,6 @@ class FlintApi: model = self.__flintModel return model.blissSessionName() - def wait_data(self, master, plot_type, index): - ev = ( - self.data_event[master] - .setdefault(plot_type, {}) - .setdefault(index, gevent.event.Event()) - ) - ev.wait(timeout=3) - def get_live_scan_data(self, channel_name): scan = self.__flintModel.currentScan() if isinstance(scan, scan_model.ScanGroup): @@ -593,41 +580,28 @@ class FlintApi: pass def update_data(self, plot_id, field, data): - self.data_dict[plot_id][field] = data + custom_plot = self._get_plot_widget(plot_id, live_plot=False) + custom_plot.updateData(field, data) def remove_data(self, plot_id, field): - del self.data_dict[plot_id][field] + custom_plot = self._get_plot_widget(plot_id, live_plot=False) + custom_plot.removeData(field) def get_data(self, plot_id, field=None): - if field is None: - return self.data_dict[plot_id] - else: - return self.data_dict[plot_id].get(field, []) + custom_plot = self._get_plot_widget(plot_id, live_plot=False) + return custom_plot.getData(field) def select_data(self, plot_id, method, names, kwargs): - plot = self._get_plot_widget(plot_id, live_plot=False) - silxPlot = plot._silxPlot() - - # Hackish legend handling - if "legend" not in kwargs and method.startswith("add"): - kwargs["legend"] = " -> ".join(names) - # Get the data to plot - args = tuple(self.data_dict[plot_id][name] for name in names) - method = getattr(silxPlot, method) - # Plot - method(*args, **kwargs) + custom_plot = self._get_plot_widget(plot_id, live_plot=False) + return custom_plot.selectData(method, names, kwargs) def deselect_data(self, plot_id, names): - plot = self._get_plot_widget(plot_id, live_plot=False) - silxPlot = plot._silxPlot() - legend = " -> ".join(names) - silxPlot.remove(legend) + custom_plot = self._get_plot_widget(plot_id, live_plot=False) + return custom_plot.deselectData(names) def clear_data(self, plot_id): - self.data_dict[plot_id].clear() - plot = self._get_plot_widget(plot_id, live_plot=False) - silxPlot = plot._silxPlot() - silxPlot.clear() + custom_plot = self._get_plot_widget(plot_id, live_plot=False) + return custom_plot.clearData() def start_image_monitoring(self, channel_name, tango_address): """Start monitoring of an image from a Tango detector. diff --git a/bliss/flint/widgets/custom_plot.py b/bliss/flint/widgets/custom_plot.py index 45118a9b92..86beefaeff 100644 --- a/bliss/flint/widgets/custom_plot.py +++ b/bliss/flint/widgets/custom_plot.py @@ -14,6 +14,12 @@ _logger = logging.getLogger(__name__) class CustomPlot(qt.QWidget): + """ + Widget holder to contain plot managed by BLISS. + + It provides few helpers to identify and interact with it. + """ + def __init__(self, parent=None): super(CustomPlot, self).__init__(parent=parent) layout = qt.QVBoxLayout(self) @@ -21,6 +27,7 @@ class CustomPlot(qt.QWidget): self.__plot = None self.__plotId = None self.__name = None + self.__data = {} def setName(self, name): self.__name = name @@ -34,10 +41,46 @@ class CustomPlot(qt.QWidget): def plotId(self): return self.__plotId - def setPlot(self, plot): + def setPlot(self, plot: qt.QWidget): + """ + Set a plot to this custom plot holder. + """ + # FIXME: Remove the previous one if there was one layout = self.layout() layout.addWidget(plot) self.__plot = plot def _silxPlot(self): return self.__plot + + def updateData(self, field, data): + self.__data[field] = data + + def removeData(self, field): + del self.__data[field] + + def getData(self, field=None): + if field is None: + return self.__data + else: + return self.__data.get(field, []) + + def selectData(self, method, names, kwargs): + # FIXME: method is not needed, that's ugly + # FIXME: kwargs is not a good idea + # Hackish legend handling + if "legend" not in kwargs and method.startswith("add"): + kwargs["legend"] = " -> ".join(names) + # Get the data to plot + args = tuple(self.__data[name] for name in names) + method = getattr(self.__plot, method) + # Plot + method(*args, **kwargs) + + def deselectData(self, names): + legend = " -> ".join(names) + self.__plot.remove(legend) + + def clearData(self): + self.__data.clear() + self.__plot.clear() -- GitLab From 16e3f9306b68c2b893d1d077f76119646510399e Mon Sep 17 00:00:00 2001 From: Valentin Valls Date: Sat, 31 Oct 2020 23:33:02 +0100 Subject: [PATCH 04/18] Fix widget name --- bliss/flint/client/plots.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bliss/flint/client/plots.py b/bliss/flint/client/plots.py index acc394df78..720a30dbd4 100644 --- a/bliss/flint/client/plots.py +++ b/bliss/flint/client/plots.py @@ -384,7 +384,7 @@ class CurvePlot(BasePlot): class ScatterPlot(BasePlot): # Name of the corresponding silx widget - WIDGET = "Plot1D" + WIDGET = "ScatterView" # Name of the method to add data to the plot METHOD = "addScatter" -- GitLab From 648b13098efbdb08169255bb0b3974d2133bf6b5 Mon Sep 17 00:00:00 2001 From: Valentin Valls Date: Sat, 31 Oct 2020 23:31:11 +0100 Subject: [PATCH 05/18] Remove dead code --- bliss/common/plot.py | 9 --------- bliss/flint/client/plots.py | 18 ------------------ bliss/flint/client/proxy.py | 1 - 3 files changed, 28 deletions(-) diff --git a/bliss/common/plot.py b/bliss/common/plot.py index 84eb9b6633..4732e7d1a0 100755 --- a/bliss/common/plot.py +++ b/bliss/common/plot.py @@ -39,13 +39,6 @@ This interface supports several types of plot: * two histograms along the X and Y dimensions are displayed * the plot is created using ``plot_image_with_histogram`` -- **curve list plot**: - - * plot a single list of 1D data as curves - * a slider and an envelop view are provided - * the plot is created using ``plot_curve_list`` - * this widget is not integrated yet! - - **image stack plot**: * plot a single stack of image @@ -162,7 +155,6 @@ from bliss.flint.client.proxy import FLINT_OUTPUT_LOGGER # noqa: F401 __all__ = [ "plot", "plot_curve", - "plot_curve_list", "plot_image", "plot_scatter", "plot_image_with_histogram", @@ -202,7 +194,6 @@ def _create_plot( plot_curve = functools.partial(_create_plot, flint_plots.CurvePlot) -plot_curve_list = functools.partial(_create_plot, flint_plots.CurveListPlot) plot_scatter = functools.partial(_create_plot, flint_plots.ScatterPlot) plot_image = functools.partial(_create_plot, flint_plots.ImagePlot) plot_image_with_histogram = functools.partial( diff --git a/bliss/flint/client/plots.py b/bliss/flint/client/plots.py index 720a30dbd4..3e1cbb0f1a 100644 --- a/bliss/flint/client/plots.py +++ b/bliss/flint/client/plots.py @@ -408,24 +408,6 @@ class McaPlot(CurvePlot): pass -class CurveListPlot(BasePlot): - - # Name of the corresponding silx widget - WIDGET = "CurvesView" - - # Name of the method to add data to the plot - METHOD = None - - # The dimension of the data to plot - DATA_DIMENSIONS = (2,) - - # Single / Multiple data handling - MULTIPLE = False - - # Data input number for a single representation - DATA_INPUT_NUMBER = 1 - - class ImagePlot(BasePlot): # Name of the corresponding silx widget diff --git a/bliss/flint/client/proxy.py b/bliss/flint/client/proxy.py index 83ae65ef5c..fb0916dc80 100644 --- a/bliss/flint/client/proxy.py +++ b/bliss/flint/client/proxy.py @@ -515,7 +515,6 @@ class FlintClient: if isinstance(plot_class, str): classes = [ plots.CurvePlot, - plots.CurveListPlot, plots.HistogramImagePlot, plots.ImagePlot, plots.ImageStackPlot, -- GitLab From c30cb06c8b15425c27d6a91ba5f497bf549add3d Mon Sep 17 00:00:00 2001 From: Valentin Valls Date: Sun, 1 Nov 2020 14:02:55 +0100 Subject: [PATCH 06/18] Use silx plot name and full class name --- bliss/flint/client/plots.py | 56 ++++++++++++++++++++++++++--------- bliss/flint/client/proxy.py | 31 ++++++++++--------- bliss/flint/flint_api.py | 28 +++++++++++++----- tests/flint/test_flint_api.py | 8 ++--- tests/flint/test_flint_gui.py | 10 +++---- 5 files changed, 87 insertions(+), 46 deletions(-) diff --git a/bliss/flint/client/plots.py b/bliss/flint/client/plots.py index 3e1cbb0f1a..35605142d4 100644 --- a/bliss/flint/client/plots.py +++ b/bliss/flint/client/plots.py @@ -24,6 +24,9 @@ class BasePlot(object): # Name of the corresponding silx widget WIDGET = NotImplemented + # Available name to identify this plot + ALIASES = [] + # Name of the method to add data to the plot METHOD = NotImplemented @@ -286,10 +289,13 @@ class BasePlot(object): # Plot classes -class CurvePlot(BasePlot): +class Plot1D(BasePlot): # Name of the corresponding silx widget - WIDGET = "Plot1D" + WIDGET = "silx.gui.plot.Plot1D" + + # Available name to identify this plot + ALIASES = ["curve", "plot1d"] # Name of the method to add data to the plot METHOD = "addCurve" @@ -381,10 +387,13 @@ class CurvePlot(BasePlot): flint.run_method(self._plot_id, "setYAxisLogarithmic", [value == "log"], {}) -class ScatterPlot(BasePlot): +class ScatterView(BasePlot): # Name of the corresponding silx widget - WIDGET = "ScatterView" + WIDGET = "silx.gui.plot.ScatterView" + + # Available name to identify this plot + ALIASES = ["scatter"] # Name of the method to add data to the plot METHOD = "addScatter" @@ -404,14 +413,13 @@ class ScatterPlot(BasePlot): self.set_colormap = self._set_colormap -class McaPlot(CurvePlot): - pass - - -class ImagePlot(BasePlot): +class Plot2D(BasePlot): # Name of the corresponding silx widget - WIDGET = "Plot2D" + WIDGET = "silx.gui.plot.Plot2D" + + # Available name to identify this plot + ALIASES = ["image", "plot2d"] # Name of the method to add data to the plot METHOD = "addImage" @@ -448,10 +456,13 @@ class ImagePlot(BasePlot): return self._wait_for_user_selection(request_id) -class HistogramImagePlot(BasePlot): +class ImageView(BasePlot): # Name of the corresponding silx widget - WIDGET = "ImageView" + WIDGET = "silx.gui.plot.ImageView" + + # Available name to identify this plot + ALIASES = ["imageview", "histogramimage"] # Name of the method to add data to the plot METHOD = "setImage" @@ -466,10 +477,13 @@ class HistogramImagePlot(BasePlot): DATA_INPUT_NUMBER = 1 -class ImageStackPlot(BasePlot): +class StackView(BasePlot): # Name of the corresponding silx widget - WIDGET = "StackView" + WIDGET = "silx.gui.plot.StackView" + + # Available name to identify this plot + ALIASES = ["stack", "imagestack"] # Name of the method to add data to the plot METHOD = "setStack" @@ -482,3 +496,17 @@ class ImageStackPlot(BasePlot): # Data input number for a single representation DATA_INPUT_NUMBER = 1 + + +class McaPlot(Plot1D): + pass + + +CUSTOM_CLASSES = [Plot1D, Plot2D, ScatterView, ImageView, StackView] + +# For compatibility +CurvePlot = Plot1D +ImagePlot = Plot2D +ScatterPlot = ScatterView +HistogramImagePlot = ImageView +ImageStackPlot = StackView diff --git a/bliss/flint/client/proxy.py b/bliss/flint/client/proxy.py index fb0916dc80..33f966351b 100644 --- a/bliss/flint/client/proxy.py +++ b/bliss/flint/client/proxy.py @@ -454,9 +454,8 @@ class FlintClient: Arguments: plot_class: A class defined in `bliss.flint.client.plot`, or a - silx class name. Can be one of "PlotWidget", - "PlotWindow", "Plot1D", "Plot2D", "ImageView", "StackView", - "ScatterView". + silx class name. Can be one of "Plot1D", "Plot2D", "ImageView", + "StackView", "ScatterView". name: Name of the plot as displayed in the tab header. It is not a unique name. unique_name: If defined the plot can be retrieved from flint. @@ -464,7 +463,7 @@ class FlintClient: displayed plot. closeable: If true (default), the tab can be closed manually """ - silx_class_name, plot_class = self.__get_plot_info(plot_class) + plot_class = self.__normalize_plot_class(plot_class) # FIXME: Hack for now, i would prefer to provide a get_live_plot for that if isinstance(unique_name, str) and unique_name.startswith("live:"): @@ -476,6 +475,7 @@ class FlintClient: if self.is_plot_exists(flint_plot_id): return plot_class(flint=self, plot_id=flint_plot_id) + silx_class_name = plot_class.WIDGET plot_id = self._proxy.add_plot( silx_class_name, name=name, selected=selected, closeable=closeable ) @@ -505,29 +505,28 @@ class FlintClient: displayed plot. closeable: If true (default), the tab can be closed manually """ - silx_class_name, plot_class = self.__get_plot_info(plot_class) + plot_class = self.__normalize_plot_class(plot_class) + silx_class_name = plot_class.WIDGET plot_id = self._proxy.add_plot( silx_class_name, name=name, selected=selected, closeable=closeable ) return plot_class(plot_id=plot_id, flint=self) - def __get_plot_info(self, plot_class): + def __normalize_plot_class(self, plot_class: typing.Union[str, object]): + """Returns a BLISS side plot class. + + Arguments: + plot_class: A BLISS side plot class, or one of its alias + """ if isinstance(plot_class, str): - classes = [ - plots.CurvePlot, - plots.HistogramImagePlot, - plots.ImagePlot, - plots.ImageStackPlot, - plots.ScatterPlot, - ] plot_class = plot_class.lower() - for cls in classes: - if cls.WIDGET.lower() == plot_class: + for cls in plots.CUSTOM_CLASSES: + if plot_class in cls.ALIASES: plot_class = cls break else: raise ValueError(f"Name '{plot_class}' does not refer to a plot class") - return plot_class.WIDGET, plot_class + return plot_class def _get_beacon_config(): diff --git a/bliss/flint/flint_api.py b/bliss/flint/flint_api.py index cda7340e1c..4e4d80ff58 100755 --- a/bliss/flint/flint_api.py +++ b/bliss/flint/flint_api.py @@ -18,12 +18,12 @@ from typing import Optional import sys import logging +import importlib import itertools import functools import numpy from silx.gui import qt -from silx.gui import plot as silx_plot import bliss from bliss.flint.helper import plot_interaction, scan_info_helper from bliss.controllers.lima import roi as lima_roi @@ -398,7 +398,7 @@ class FlintApi: def add_plot( self, - cls_name: str, + class_name: str, name: str = None, selected: bool = False, closeable: bool = True, @@ -408,9 +408,10 @@ class FlintApi: The plot will be created in a new tab on Flint. Arguments: - cls_name: A class name defined by silx. Can be one of "PlotWidget", - "PlotWindow", "Plot1D", "Plot2D", "ImageView", "StackView", - "ScatterView". + class_name: A class to display a plot. Can be one of: + "silx.gui.plot.Plot1D", "silx.gui.plot.Plot2D", + "silx.gui.plot.ImageView", "silx.gui.plot.StackView", + "silx.gui.plot.ScatterView". name: Name of the plot as displayed in the tab header. It is not a unique name. selected: If true (not the default) the plot became the current @@ -423,9 +424,22 @@ class FlintApi: plot_id = self.create_new_id() if not name: name = "Plot %d" % plot_id + + def get_class(class_name): + try: + module_name, class_name = class_name.rsplit(".", 1) + module = importlib.import_module(module_name) + class_obj = getattr(module, class_name) + return class_obj + except Exception: + _logger.debug( + "Error while reaching class name '%s'", class_name, exc_info=True + ) + raise ValueError("Unknown class name %s" % class_name) + + class_obj = get_class(class_name) window = self.__flintModel.mainWindow() - cls = getattr(silx_plot, cls_name) - plot = cls(window) + plot = class_obj(parent=window) window.createCustomPlot( plot, name, plot_id, selected=selected, closeable=closeable ) diff --git a/tests/flint/test_flint_api.py b/tests/flint/test_flint_api.py index 9ae20d4b50..254a7617e5 100755 --- a/tests/flint/test_flint_api.py +++ b/tests/flint/test_flint_api.py @@ -28,10 +28,10 @@ class TestFlint(TestCaseQt): assert p.name == "Some name" def test_remove_custom_plot(self): - widget = plots.CurvePlot(name="foo-rm") - plot_id = widget.plot_id - flint_api = widget._flint - flint_api.remove_plot(plot_id) + flint = plot.get_flint() + p = flint.get_plot(plot_class="curve", name="foo-rm") + flint.remove_plot(p.plot_id) + assert flint.is_plot_exists("foo-rm") is False def test_custom_plot_curveplot(self): widget = plots.CurvePlot(name="foo") diff --git a/tests/flint/test_flint_gui.py b/tests/flint/test_flint_gui.py index b7cce60997..bde60d1a2f 100755 --- a/tests/flint/test_flint_gui.py +++ b/tests/flint/test_flint_gui.py @@ -18,7 +18,7 @@ def test_empty_plot(flint_session): def test_simple_plot(flint_session): sin = flint_session.env_dict["sin_data"] p = plot.plot(sin) - assert "CurvePlot" in repr(p) + assert "Plot1D" in repr(p) data = p.get_data() assert data == { "default": pytest.approx(sin), @@ -30,7 +30,7 @@ def test_plot_curve_with_x(flint_session): sin = flint_session.env_dict["sin_data"] cos = flint_session.env_dict["cos_data"] p = plot.plot({"sin": sin, "cos": cos}, x="sin") - assert "CurvePlot" in repr(p) + assert "Plot1D" in repr(p) data = p.get_data() assert data == {"sin": pytest.approx(sin), "cos": pytest.approx(cos)} @@ -38,12 +38,12 @@ def test_plot_curve_with_x(flint_session): def test_image_plot(flint_session): grey_image = flint_session.env_dict["grey_image"] p = plot.plot(grey_image) - assert "ImagePlot" in repr(p) + assert "Plot2D" in repr(p) data = p.get_data() assert data == {"default": pytest.approx(grey_image)} colored_image = flint_session.env_dict["colored_image"] p = plot.plot(colored_image) - assert "ImagePlot" in repr(p) + assert "Plot2D" in repr(p) data = p.get_data() assert data == {"default": pytest.approx(colored_image)} @@ -54,7 +54,7 @@ def test_curve_plot(flint_session): scan = flint_session.env_dict["sin_cos_scan"] for sin_cos in (dct, struct, scan): p = plot.plot(sin_cos) - assert "CurvePlot" in repr(p) + assert "Plot1D" in repr(p) data = p.get_data() assert data == { "x": pytest.approx(sin_cos["x"]), -- GitLab From 46741f478bd926be27441077ec3c3d3872dee851 Mon Sep 17 00:00:00 2001 From: Valentin Valls Date: Sun, 1 Nov 2020 14:24:07 +0100 Subject: [PATCH 07/18] Split live plot from BLISS plot definition --- bliss/flint/client/plots.py | 30 ++++++++++++++++++++++++++++-- bliss/flint/client/proxy.py | 8 ++++---- 2 files changed, 32 insertions(+), 6 deletions(-) diff --git a/bliss/flint/client/plots.py b/bliss/flint/client/plots.py index 35605142d4..f1df48efc5 100644 --- a/bliss/flint/client/plots.py +++ b/bliss/flint/client/plots.py @@ -498,12 +498,38 @@ class StackView(BasePlot): DATA_INPUT_NUMBER = 1 -class McaPlot(Plot1D): - pass +class LiveCurvePlot(Plot1D): + + WIDGET = None + + ALIASES = ["curve"] + + +class LiveImagePlot(Plot2D): + + WIDGET = None + + ALIASES = ["image"] + + +class LiveScatterPlot(Plot1D): + + WIDGET = None + + ALIASES = ["scatter"] + + +class LiveMcaPlot(Plot1D): + + WIDGET = None + + ALIASES = ["mca"] CUSTOM_CLASSES = [Plot1D, Plot2D, ScatterView, ImageView, StackView] +LIVE_CLASSES = [LiveCurvePlot, LiveImagePlot, LiveScatterPlot, LiveMcaPlot] + # For compatibility CurvePlot = Plot1D ImagePlot = Plot2D diff --git a/bliss/flint/client/proxy.py b/bliss/flint/client/proxy.py index 33f966351b..4da6c01c27 100644 --- a/bliss/flint/client/proxy.py +++ b/bliss/flint/client/proxy.py @@ -414,10 +414,10 @@ class FlintClient: """ if kind is not None: if kind == "default-curve": - plot_class = plots.CurvePlot + plot_class = plots.LiveCurvePlot plot_type = "curve" elif kind == "default-scatter": - plot_class = plots.ScatterPlot + plot_class = plots.LiveScatterPlot plot_type = "scatter" else: raise ValueError(f"Unexpected plot kind '{kind}'.") @@ -429,12 +429,12 @@ class FlintClient: return plot_class(plot_id=plot_id, flint=self) elif image_detector is not None: - plot_class = plots.ImagePlot + plot_class = plots.LiveImagePlot plot_id = self.get_live_plot_detector(image_detector, plot_type="image") return plot_class(plot_id=plot_id, flint=self) elif mca_detector is not None: - plot_class = plots.McaPlot + plot_class = plots.LiveMcaPlot plot_id = self.get_live_plot_detector(mca_detector, plot_type="mca") return plot_class(plot_id=plot_id, flint=self) -- GitLab From 6cf85a64e4a38a76545918eda1100e3f360b4536 Mon Sep 17 00:00:00 2001 From: Valentin Valls Date: Sun, 1 Nov 2020 19:54:38 +0100 Subject: [PATCH 08/18] Create a FlintClientMock for unittests --- bliss/flint/client/proxy.py | 2 ++ tests/flint/conftest.py | 11 ++++++++--- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/bliss/flint/client/proxy.py b/bliss/flint/client/proxy.py index 4da6c01c27..c4c7a059c7 100644 --- a/bliss/flint/client/proxy.py +++ b/bliss/flint/client/proxy.py @@ -63,7 +63,9 @@ class FlintClient: """Store mapping from name to int id. This should be part of flint_api at one point. """ + self._init(process) + def _init(self, process): if process is None: self.__start_flint() else: diff --git a/tests/flint/conftest.py b/tests/flint/conftest.py index 2b0283880f..6035d27636 100644 --- a/tests/flint/conftest.py +++ b/tests/flint/conftest.py @@ -15,13 +15,18 @@ def _get_real_flint(*args, **kwargs): """Replacement function for monkey patch of `bliss.common.plot`""" from bliss.flint import flint from silx.gui import qt + from bliss.flint.client.proxy import FlintClient flint.initApplication([]) settings = qt.QSettings() flint_model = flint.create_flint_model(settings) - interface = flint_model.flintApi() - interface._pid = -666 - return interface + + class FlintClientMock(FlintClient): + def _init(self, process): + self._proxy = flint_model.flintApi() + self._pid = -666 + + return FlintClientMock() @contextmanager -- GitLab From 603e56642ed934a7ad112cec300f3c267655fc56 Mon Sep 17 00:00:00 2001 From: Valentin Valls Date: Sun, 1 Nov 2020 19:56:47 +0100 Subject: [PATCH 09/18] Use common flint session for custom plot tests --- tests/flint/test_flint_api.py | 27 +++++++++++---------------- 1 file changed, 11 insertions(+), 16 deletions(-) diff --git a/tests/flint/test_flint_api.py b/tests/flint/test_flint_api.py index 254a7617e5..2268aac8e5 100755 --- a/tests/flint/test_flint_api.py +++ b/tests/flint/test_flint_api.py @@ -14,18 +14,12 @@ from bliss.controllers.lima import roi as lima_roi logger = logging.getLogger(__name__) -@pytest.mark.skip(reason="This test often segfault the bcu-ci") -@pytest.mark.usefixtures("flint_norpc") +@pytest.mark.usefixtures("flint_session") class TestFlint(TestCaseQt): def test_empty_plot(self): - p = plot.plot() - pid = plot.get_flint()._pid - assert "flint_pid={}".format(pid) in repr(p) - assert p.name == "Plot {}".format(p._plot_id) - - p = plot.plot(name="Some name") - assert "flint_pid={}".format(pid) in repr(p) - assert p.name == "Some name" + flint = plot.get_flint() + p = flint.get_plot(plot_class="curve", name="foo-empty") + assert p is not None def test_remove_custom_plot(self): flint = plot.get_flint() @@ -34,16 +28,17 @@ class TestFlint(TestCaseQt): assert flint.is_plot_exists("foo-rm") is False def test_custom_plot_curveplot(self): - widget = plots.CurvePlot(name="foo") + flint = plot.get_flint() + p = flint.get_plot(plot_class="curve", name="foo-cp") cos_data = numpy.cos(numpy.linspace(0, 2 * numpy.pi, 10)) sin_data = numpy.sin(numpy.linspace(0, 2 * numpy.pi, 10)) - widget.add_data({"cos": cos_data, "sin": sin_data}) - widget.select_data("sin", "cos") - widget.select_data("sin", "cos", color="green", symbol="x") - widget.deselect_data("sin", "cos") - widget.clear_data() + p.add_data({"cos": cos_data, "sin": sin_data}) + p.select_data("sin", "cos") + p.select_data("sin", "cos", color="green", symbol="x") + p.deselect_data("sin", "cos") + p.clear_data() def test_used_object(): -- GitLab From ae1fbb5035271935a331e9a46f5c8104da89d3e2 Mon Sep 17 00:00:00 2001 From: Valentin Valls Date: Sun, 1 Nov 2020 19:59:59 +0100 Subject: [PATCH 10/18] Allow to transfer RPC functions for custom plots --- bliss/flint/client/plots.py | 15 +++++++++++++++ bliss/flint/flint_api.py | 16 ++++++++++++++++ bliss/flint/widgets/custom_plot.py | 15 +++++++++++++++ 3 files changed, 46 insertions(+) diff --git a/bliss/flint/client/plots.py b/bliss/flint/client/plots.py index f1df48efc5..5435801431 100644 --- a/bliss/flint/client/plots.py +++ b/bliss/flint/client/plots.py @@ -14,6 +14,7 @@ from typing import Optional import numpy import gevent +import marshal from . import proxy from bliss.common import event @@ -245,6 +246,20 @@ class BasePlot(object): request_id = flint.request_select_shape(self._plot_id, shape) return self._wait_for_user_selection(request_id) + def _remotify(self, func): + """Make a function callable remotely""" + method_id = func.__qualname__ + plot_id = self._plot_id + if func.__closure__: + raise TypeError("Only function without closure are supported.") + serialized_func = marshal.dumps(func.__code__) + self._flint.register_custom_method(plot_id, method_id, serialized_func) + + def handler(*args, **kwargs): + return self._flint.run_custom_method(plot_id, method_id, args, kwargs) + + return handler + def _set_colormap( self, lut: Optional[str] = None, diff --git a/bliss/flint/flint_api.py b/bliss/flint/flint_api.py index 4e4d80ff58..d7b5d49ffc 100755 --- a/bliss/flint/flint_api.py +++ b/bliss/flint/flint_api.py @@ -17,11 +17,13 @@ from typing import NamedTuple from typing import Optional import sys +import types import logging import importlib import itertools import functools import numpy +import marshal from silx.gui import qt import bliss @@ -297,6 +299,20 @@ class FlintApi: method = getattr(silxPlot, method) return method(*args, **kwargs) + def run_custom_method(self, plot_id, method_id, args, kwargs): + """Run a registered method from a custom plot. + """ + plot = self._get_plot_widget(plot_id, live_plot=False) + return plot.runMethod(method_id, args, kwargs) + + def register_custom_method(self, plot_id, method_id, serialized_method): + """Register a method to a custom plot. + """ + plot = self._get_plot_widget(plot_id, live_plot=False) + code = marshal.loads(serialized_method) + method = types.FunctionType(code, globals(), "deserialized_function") + plot.registerMethod(method_id, method) + def ping(self, msg=None, stderr=False): """Debug function to check writing on stdout/stderr remotely.""" if stderr: diff --git a/bliss/flint/widgets/custom_plot.py b/bliss/flint/widgets/custom_plot.py index 86beefaeff..7671bf7ecd 100644 --- a/bliss/flint/widgets/custom_plot.py +++ b/bliss/flint/widgets/custom_plot.py @@ -28,6 +28,7 @@ class CustomPlot(qt.QWidget): self.__plotId = None self.__name = None self.__data = {} + self.__methods = {} def setName(self, name): self.__name = name @@ -53,6 +54,20 @@ class CustomPlot(qt.QWidget): def _silxPlot(self): return self.__plot + def registerMethod(self, method_id, method): + if method_id in self.__methods: + raise ValueError(f"Method {method_id} already registred") + self.__methods[method_id] = method + + def runMethod(self, method_id, args, kwargs): + method = self.__methods.get(method_id) + if method_id is None: + plot_id = self.plotId() + raise ValueError( + "Method '%s' on plot id '%s' is unknown", method_id, plot_id + ) + return method(self, self.__plot, self.__data, args, kwargs) + def updateData(self, field, data): self.__data[field] = data -- GitLab From 24a447291f29581c88879b20ffdfbe39895ea2a5 Mon Sep 17 00:00:00 2001 From: Valentin Valls Date: Sun, 1 Nov 2020 20:53:00 +0100 Subject: [PATCH 11/18] Use transfered function instead of dedicated flint API --- bliss/flint/client/plots.py | 62 ++++++++++++++++++++++++++---- bliss/flint/flint_api.py | 24 ------------ bliss/flint/widgets/custom_plot.py | 26 ------------- 3 files changed, 55 insertions(+), 57 deletions(-) diff --git a/bliss/flint/client/plots.py b/bliss/flint/client/plots.py index 5435801431..ab301ea8fa 100644 --- a/bliss/flint/client/plots.py +++ b/bliss/flint/client/plots.py @@ -52,6 +52,54 @@ class BasePlot(object): def _init_plot(self): """Inherits it to custom the plot initialization""" + + @self._remotify + def select_data(holder, widget, data_dict, args, kwargs): + method, names, kwargs = args + if "legend" not in kwargs and method.startswith("add"): + kwargs["legend"] = " -> ".join(names) + # Get the data to plot + args = tuple(data_dict[name] for name in names) + widget_method = getattr(widget, method) + # Plot + widget_method(*args, **kwargs) + + @self._remotify + def update_data(holder, widget, data_dict, args, kwargs): + field, data = args + data_dict[field] = data + + @self._remotify + def remove_data(holder, widget, data_dict, args, kwargs): + field = args[0] + data_dict[field] + + @self._remotify + def get_data(holder, widget, data_dict, args, kwargs): + field = args[0] + if field is None: + return data_dict + else: + return data_dict.get(field, []) + + @self._remotify + def deselect_data(holder, widget, data_dict, args, kwargs): + names = args[0] + legend = " -> ".join(names) + widget.remove(legend) + + @self._remotify + def clear_data(holder, widget, data_dict, args, kwargs): + data_dict.clear() + widget.clear() + + self.__select_data = select_data + self.__update_data = update_data + self.__remove_data = remove_data + self.__get_data = get_data + self.__deselect_data = deselect_data + self.__clear_data = clear_data + if self._xlabel is not None: self.submit("setGraphXLabel", self._xlabel) if self._ylabel is not None: @@ -120,7 +168,7 @@ class BasePlot(object): self.DATA_DIMENSIONS, data.ndim ) ) - return self._flint.update_data(self._plot_id, field, data) + return self.__update_data(field, data) def add_data(self, data, field="default"): # Get fields @@ -141,19 +189,19 @@ class BasePlot(object): return data_dict def remove_data(self, field): - return self._flint.remove_data(self._plot_id, field) + self.__remove_data(field) def select_data(self, *names, **kwargs): - return self._flint.select_data(self._plot_id, self.METHOD, names, kwargs) + self.__select_data(self.METHOD, names, kwargs) def deselect_data(self, *names): - return self._flint.deselect_data(self._plot_id, names) + self.__deselect_data(names) def clear_data(self): - return self._flint.clear_data(self._plot_id) + self.__clear_data() - def get_data(self): - return self._flint.get_data(self._plot_id) + def get_data(self, field=None): + return self.__get_data(field) # Plotting diff --git a/bliss/flint/flint_api.py b/bliss/flint/flint_api.py index d7b5d49ffc..b687777bbb 100755 --- a/bliss/flint/flint_api.py +++ b/bliss/flint/flint_api.py @@ -609,30 +609,6 @@ class FlintApi: # Nothing to do pass - def update_data(self, plot_id, field, data): - custom_plot = self._get_plot_widget(plot_id, live_plot=False) - custom_plot.updateData(field, data) - - def remove_data(self, plot_id, field): - custom_plot = self._get_plot_widget(plot_id, live_plot=False) - custom_plot.removeData(field) - - def get_data(self, plot_id, field=None): - custom_plot = self._get_plot_widget(plot_id, live_plot=False) - return custom_plot.getData(field) - - def select_data(self, plot_id, method, names, kwargs): - custom_plot = self._get_plot_widget(plot_id, live_plot=False) - return custom_plot.selectData(method, names, kwargs) - - def deselect_data(self, plot_id, names): - custom_plot = self._get_plot_widget(plot_id, live_plot=False) - return custom_plot.deselectData(names) - - def clear_data(self, plot_id): - custom_plot = self._get_plot_widget(plot_id, live_plot=False) - return custom_plot.clearData() - def start_image_monitoring(self, channel_name, tango_address): """Start monitoring of an image from a Tango detector. diff --git a/bliss/flint/widgets/custom_plot.py b/bliss/flint/widgets/custom_plot.py index 7671bf7ecd..e9e48dab4d 100644 --- a/bliss/flint/widgets/custom_plot.py +++ b/bliss/flint/widgets/custom_plot.py @@ -68,34 +68,8 @@ class CustomPlot(qt.QWidget): ) return method(self, self.__plot, self.__data, args, kwargs) - def updateData(self, field, data): - self.__data[field] = data - - def removeData(self, field): - del self.__data[field] - def getData(self, field=None): if field is None: return self.__data else: return self.__data.get(field, []) - - def selectData(self, method, names, kwargs): - # FIXME: method is not needed, that's ugly - # FIXME: kwargs is not a good idea - # Hackish legend handling - if "legend" not in kwargs and method.startswith("add"): - kwargs["legend"] = " -> ".join(names) - # Get the data to plot - args = tuple(self.__data[name] for name in names) - method = getattr(self.__plot, method) - # Plot - method(*args, **kwargs) - - def deselectData(self, names): - legend = " -> ".join(names) - self.__plot.remove(legend) - - def clearData(self): - self.__data.clear() - self.__plot.clear() -- GitLab From e966334c070b2cf58cd66fe982f8c37eed9fadf6 Mon Sep 17 00:00:00 2001 From: Valentin Valls Date: Sat, 5 Dec 2020 17:35:36 +0100 Subject: [PATCH 12/18] Expose custom plot logger --- bliss/flint/widgets/custom_plot.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/bliss/flint/widgets/custom_plot.py b/bliss/flint/widgets/custom_plot.py index e9e48dab4d..876aba366f 100644 --- a/bliss/flint/widgets/custom_plot.py +++ b/bliss/flint/widgets/custom_plot.py @@ -42,6 +42,10 @@ class CustomPlot(qt.QWidget): def plotId(self): return self.__plotId + def getLogger(self): + global _logger + return _logger + def setPlot(self, plot: qt.QWidget): """ Set a plot to this custom plot holder. -- GitLab From ed0522c17f68901788575f2e0355438e4a509faa Mon Sep 17 00:00:00 2001 From: Valentin Valls Date: Sat, 5 Dec 2020 18:43:55 +0100 Subject: [PATCH 13/18] Classify remote API --- bliss/flint/client/plots.py | 72 ++++++++++++++++-------------- bliss/flint/widgets/custom_plot.py | 10 ++++- 2 files changed, 47 insertions(+), 35 deletions(-) diff --git a/bliss/flint/client/plots.py b/bliss/flint/client/plots.py index ab301ea8fa..529f76ad95 100644 --- a/bliss/flint/client/plots.py +++ b/bliss/flint/client/plots.py @@ -15,6 +15,7 @@ from typing import Optional import numpy import gevent import marshal +import inspect from . import proxy from bliss.common import event @@ -50,56 +51,49 @@ class BasePlot(object): if flint is not None: self._init_plot() - def _init_plot(self): - """Inherits it to custom the plot initialization""" + class RemotePlot: + """This class is serialized method by method and executed inside the Flint context""" - @self._remotify - def select_data(holder, widget, data_dict, args, kwargs): - method, names, kwargs = args + def select_data(self, method, *names, **kwargs): if "legend" not in kwargs and method.startswith("add"): kwargs["legend"] = " -> ".join(names) # Get the data to plot + data_dict = self.data() + widget = self.widget() args = tuple(data_dict[name] for name in names) widget_method = getattr(widget, method) # Plot widget_method(*args, **kwargs) - @self._remotify - def update_data(holder, widget, data_dict, args, kwargs): - field, data = args + def update_data(self, field, data): + data_dict = self.data() data_dict[field] = data - @self._remotify - def remove_data(holder, widget, data_dict, args, kwargs): - field = args[0] + def remove_data(self, field): + data_dict = self.data() data_dict[field] - @self._remotify - def get_data(holder, widget, data_dict, args, kwargs): - field = args[0] + def get_data(self, field=None): + data_dict = self.data() if field is None: return data_dict else: return data_dict.get(field, []) - @self._remotify - def deselect_data(holder, widget, data_dict, args, kwargs): - names = args[0] + def deselect_data(self, *names): + widget = self.widget() legend = " -> ".join(names) widget.remove(legend) - @self._remotify - def clear_data(holder, widget, data_dict, args, kwargs): + def clear_data(self): + data_dict = self.data() + widget = self.widget() data_dict.clear() widget.clear() - self.__select_data = select_data - self.__update_data = update_data - self.__remove_data = remove_data - self.__get_data = get_data - self.__deselect_data = deselect_data - self.__clear_data = clear_data - + def _init_plot(self): + """Inherits it to custom the plot initialization""" + self.__remote = self._remotifyClass(self.RemotePlot) if self._xlabel is not None: self.submit("setGraphXLabel", self._xlabel) if self._ylabel is not None: @@ -168,7 +162,7 @@ class BasePlot(object): self.DATA_DIMENSIONS, data.ndim ) ) - return self.__update_data(field, data) + return self.__remote.update_data(field, data) def add_data(self, data, field="default"): # Get fields @@ -189,19 +183,19 @@ class BasePlot(object): return data_dict def remove_data(self, field): - self.__remove_data(field) + self.__remote.remove_data(field) def select_data(self, *names, **kwargs): - self.__select_data(self.METHOD, names, kwargs) + self.__remote.select_data(self.METHOD, *names, **kwargs) def deselect_data(self, *names): - self.__deselect_data(names) + self.__remote.deselect_data(*names) def clear_data(self): - self.__clear_data() + self.__remote.clear_data() def get_data(self, field=None): - return self.__get_data(field) + return self.__remote.get_data(field=field) # Plotting @@ -294,7 +288,19 @@ class BasePlot(object): request_id = flint.request_select_shape(self._plot_id, shape) return self._wait_for_user_selection(request_id) - def _remotify(self, func): + def _remotifyClass(self, remoteClass): + class RemoteProxy: + pass + + proxy = RemoteProxy() + methods = inspect.getmembers(remoteClass, predicate=inspect.isfunction) + for name, func in methods: + handle = self._remotifyFunc(func) + setattr(proxy, name, handle) + + return proxy + + def _remotifyFunc(self, func): """Make a function callable remotely""" method_id = func.__qualname__ plot_id = self._plot_id diff --git a/bliss/flint/widgets/custom_plot.py b/bliss/flint/widgets/custom_plot.py index 876aba366f..649b958d42 100644 --- a/bliss/flint/widgets/custom_plot.py +++ b/bliss/flint/widgets/custom_plot.py @@ -42,10 +42,16 @@ class CustomPlot(qt.QWidget): def plotId(self): return self.__plotId - def getLogger(self): + def logger(self): global _logger return _logger + def widget(self): + return self.__plot + + def data(self): + return self.__data + def setPlot(self, plot: qt.QWidget): """ Set a plot to this custom plot holder. @@ -70,7 +76,7 @@ class CustomPlot(qt.QWidget): raise ValueError( "Method '%s' on plot id '%s' is unknown", method_id, plot_id ) - return method(self, self.__plot, self.__data, args, kwargs) + return method(self, *args, **kwargs) def getData(self, field=None): if field is None: -- GitLab From 0f09362d42d5aace1ee596613894e58a81d860c9 Mon Sep 17 00:00:00 2001 From: Valentin Valls Date: Sun, 6 Dec 2020 21:33:20 +0100 Subject: [PATCH 14/18] Rework remote API tests --- ...{test_flint_gui.py => test_common_plot.py} | 2 +- tests/flint/test_custom_plots.py | 35 +++++++++++++++++++ tests/flint/test_flint_api.py | 35 +------------------ 3 files changed, 37 insertions(+), 35 deletions(-) rename tests/flint/{test_flint_gui.py => test_common_plot.py} (97%) diff --git a/tests/flint/test_flint_gui.py b/tests/flint/test_common_plot.py similarity index 97% rename from tests/flint/test_flint_gui.py rename to tests/flint/test_common_plot.py index bde60d1a2f..8b0dfb0ece 100755 --- a/tests/flint/test_flint_gui.py +++ b/tests/flint/test_common_plot.py @@ -1,4 +1,4 @@ -"""Testing Flint.""" +"""Testing the BLISS bliss.common.plot API.""" import pytest from bliss.common import plot diff --git a/tests/flint/test_custom_plots.py b/tests/flint/test_custom_plots.py index 8abd3e2ea2..4c4afa9c9d 100644 --- a/tests/flint/test_custom_plots.py +++ b/tests/flint/test_custom_plots.py @@ -1,10 +1,45 @@ """Testing custom plots provided by Flint.""" +import pytest import gevent +import numpy from bliss.common import plot from bliss.controllers.lima import roi as lima_roi +def test_empty_plot(flint_session): + flint = plot.get_flint() + p = flint.get_plot(plot_class="curve", name="foo-empty") + assert flint.is_plot_exists("foo-empty") is False + assert p is not None + + +def test_remove_custom_plot(flint_session): + flint = plot.get_flint() + p = flint.get_plot(plot_class="curve", name="foo-rm") + flint.remove_plot(p.plot_id) + assert flint.is_plot_exists("foo-rm") is False + + +def test_custom_plot_curveplot(flint_session): + flint = plot.get_flint() + p = flint.get_plot(plot_class="curve", name="foo-cp") + + cos_data = numpy.cos(numpy.linspace(0, 2 * numpy.pi, 10)) + sin_data = numpy.sin(numpy.linspace(0, 2 * numpy.pi, 10)) + + p.add_data({"cos": cos_data, "sin": sin_data}) + p.select_data("sin", "cos") + p.select_data("sin", "cos", color="green", symbol="x") + p.deselect_data("sin", "cos") + p.remove_data("sin") + + data = p.get_data("cos") + assert data == pytest.approx(cos_data) + + p.clear_data() + + def test_select_points(flint_session): flint = plot.get_flint() p = plot.plot() diff --git a/tests/flint/test_flint_api.py b/tests/flint/test_flint_api.py index 2268aac8e5..5fa7bbc890 100755 --- a/tests/flint/test_flint_api.py +++ b/tests/flint/test_flint_api.py @@ -1,46 +1,13 @@ -"""Testing LogWidget.""" +"""Testing the remote API provided by Flint.""" import logging -import pytest -import numpy import pickle -from silx.gui import qt # noqa: F401 -from silx.gui.utils.testutils import TestCaseQt -from bliss.common import plot -from bliss.flint.client import plots from bliss.controllers.lima import roi as lima_roi logger = logging.getLogger(__name__) -@pytest.mark.usefixtures("flint_session") -class TestFlint(TestCaseQt): - def test_empty_plot(self): - flint = plot.get_flint() - p = flint.get_plot(plot_class="curve", name="foo-empty") - assert p is not None - - def test_remove_custom_plot(self): - flint = plot.get_flint() - p = flint.get_plot(plot_class="curve", name="foo-rm") - flint.remove_plot(p.plot_id) - assert flint.is_plot_exists("foo-rm") is False - - def test_custom_plot_curveplot(self): - flint = plot.get_flint() - p = flint.get_plot(plot_class="curve", name="foo-cp") - - cos_data = numpy.cos(numpy.linspace(0, 2 * numpy.pi, 10)) - sin_data = numpy.sin(numpy.linspace(0, 2 * numpy.pi, 10)) - - p.add_data({"cos": cos_data, "sin": sin_data}) - p.select_data("sin", "cos") - p.select_data("sin", "cos", color="green", symbol="x") - p.deselect_data("sin", "cos") - p.clear_data() - - def test_used_object(): """Make sure object shared in the RPC are still picklable""" roi = lima_roi.ArcRoi(0, 1, 2, 3, 4, 5, 6) -- GitLab From 36e2c161d7b278f3fd736f91dfc1eefcfa5ced72 Mon Sep 17 00:00:00 2001 From: Valentin Valls Date: Sun, 6 Dec 2020 22:56:32 +0100 Subject: [PATCH 15/18] Store plot name mapping inside Flint --- bliss/flint/client/plots.py | 38 +++++++++++++++++++++------------ bliss/flint/client/proxy.py | 28 +++++++++--------------- bliss/flint/flint_api.py | 18 ++++++++++------ tests/flint/test_common_plot.py | 4 ++-- 4 files changed, 47 insertions(+), 41 deletions(-) diff --git a/bliss/flint/client/plots.py b/bliss/flint/client/plots.py index 529f76ad95..d524682ffd 100644 --- a/bliss/flint/client/plots.py +++ b/bliss/flint/client/plots.py @@ -41,15 +41,21 @@ class BasePlot(object): # Data input number for a single representation DATA_INPUT_NUMBER = NotImplemented - def __init__(self, flint, plot_id): + def __init__(self, flint, plot_id, register=False): """Describe a custom plot handled by Flint. """ self._plot_id = plot_id self._flint = flint self._xlabel = None self._ylabel = None + self._init() if flint is not None: - self._init_plot() + self._register(flint, plot_id, register) + + def _init(self): + """Allow to initialize extra attributes in a derived class, without + redefining the constructor""" + pass class RemotePlot: """This class is serialized method by method and executed inside the Flint context""" @@ -91,9 +97,14 @@ class BasePlot(object): data_dict.clear() widget.clear() + def _register(self, flint, plot_id, register): + """Register everything needed remotly""" + self.__remote = self._remotifyClass(self.RemotePlot, register=register) + if register: + self._init_plot() + def _init_plot(self): """Inherits it to custom the plot initialization""" - self.__remote = self._remotifyClass(self.RemotePlot) if self._xlabel is not None: self.submit("setGraphXLabel", self._xlabel) if self._ylabel is not None: @@ -288,26 +299,27 @@ class BasePlot(object): request_id = flint.request_select_shape(self._plot_id, shape) return self._wait_for_user_selection(request_id) - def _remotifyClass(self, remoteClass): + def _remotifyClass(self, remoteClass, register=True): class RemoteProxy: pass proxy = RemoteProxy() methods = inspect.getmembers(remoteClass, predicate=inspect.isfunction) for name, func in methods: - handle = self._remotifyFunc(func) + handle = self._remotifyFunc(func, register=register) setattr(proxy, name, handle) return proxy - def _remotifyFunc(self, func): + def _remotifyFunc(self, func, register=True): """Make a function callable remotely""" method_id = func.__qualname__ plot_id = self._plot_id - if func.__closure__: - raise TypeError("Only function without closure are supported.") - serialized_func = marshal.dumps(func.__code__) - self._flint.register_custom_method(plot_id, method_id, serialized_func) + if register: + if func.__closure__: + raise TypeError("Only function without closure are supported.") + serialized_func = marshal.dumps(func.__code__) + self._flint.register_custom_method(plot_id, method_id, serialized_func) def handler(*args, **kwargs): return self._flint.run_custom_method(plot_id, method_id, args, kwargs) @@ -476,8 +488,7 @@ class ScatterView(BasePlot): # Data input number for a single representation DATA_INPUT_NUMBER = 3 - def __init__(self, flint, plot_id): - BasePlot.__init__(self, flint, plot_id) + def _init(self): # Make it public self.set_colormap = self._set_colormap @@ -502,8 +513,7 @@ class Plot2D(BasePlot): # Data input number for a single representation DATA_INPUT_NUMBER = 1 - def __init__(self, flint, plot_id): - BasePlot.__init__(self, flint, plot_id) + def _init(self): # Make it public self.set_colormap = self._set_colormap diff --git a/bliss/flint/client/proxy.py b/bliss/flint/client/proxy.py index c4c7a059c7..5a4461566d 100644 --- a/bliss/flint/client/proxy.py +++ b/bliss/flint/client/proxy.py @@ -59,10 +59,6 @@ class FlintClient: self._greenlets = None self._callbacks = None - self._plot_mapping = {} - """Store mapping from name to int id. - This should be part of flint_api at one point. - """ self._init(process) def _init(self, process): @@ -445,7 +441,7 @@ class FlintClient: def get_plot( self, plot_class: typing.Union[str, object], - name: str, + name: str = None, unique_name: str = None, selected: bool = False, closeable: bool = True, @@ -467,23 +463,19 @@ class FlintClient: """ plot_class = self.__normalize_plot_class(plot_class) - # FIXME: Hack for now, i would prefer to provide a get_live_plot for that - if isinstance(unique_name, str) and unique_name.startswith("live:"): - return plot_class(flint=self, plot_id=unique_name) - if unique_name is not None: - flint_plot_id = self._plot_mapping.get(unique_name, None) - if flint_plot_id is not None: - if self.is_plot_exists(flint_plot_id): - return plot_class(flint=self, plot_id=flint_plot_id) + if self.is_plot_exists(unique_name): + return plot_class(flint=self, plot_id=unique_name) silx_class_name = plot_class.WIDGET plot_id = self._proxy.add_plot( - silx_class_name, name=name, selected=selected, closeable=closeable + silx_class_name, + name=name, + selected=selected, + closeable=closeable, + unique_name=unique_name, ) - if unique_name is not None: - self._plot_mapping[unique_name] = plot_id - return plot_class(plot_id=plot_id, flint=self) + return plot_class(plot_id=plot_id, flint=self, register=True) def add_plot( self, @@ -512,7 +504,7 @@ class FlintClient: plot_id = self._proxy.add_plot( silx_class_name, name=name, selected=selected, closeable=closeable ) - return plot_class(plot_id=plot_id, flint=self) + return plot_class(plot_id=plot_id, flint=self, register=True) def __normalize_plot_class(self, plot_class: typing.Union[str, object]): """Returns a BLISS side plot class. diff --git a/bliss/flint/flint_api.py b/bliss/flint/flint_api.py index b687777bbb..e49a91be18 100755 --- a/bliss/flint/flint_api.py +++ b/bliss/flint/flint_api.py @@ -410,7 +410,8 @@ class FlintApi: return False else: window = self.__flintModel.mainWindow() - return window.customPlot(plot_id) is not None + custom_plot = window.customPlot(plot_id) + return custom_plot is not None def add_plot( self, @@ -418,7 +419,8 @@ class FlintApi: name: str = None, selected: bool = False, closeable: bool = True, - ): + unique_name: str = None, + ) -> str: """Create a new custom plot based on the `silx` API. The plot will be created in a new tab on Flint. @@ -433,13 +435,15 @@ class FlintApi: selected: If true (not the default) the plot became the current displayed plot. closeable: If true (default), the tab can be closed manually + unique_name: Unique name for this new plot Returns: A plot_id """ - plot_id = self.create_new_id() + if unique_name is None: + unique_name = "custom_plot:%d" % self.create_new_id() if not name: - name = "Plot %d" % plot_id + name = "%s" % unique_name def get_class(class_name): try: @@ -457,9 +461,9 @@ class FlintApi: window = self.__flintModel.mainWindow() plot = class_obj(parent=window) window.createCustomPlot( - plot, name, plot_id, selected=selected, closeable=closeable + plot, name, unique_name, selected=selected, closeable=closeable ) - return plot_id + return unique_name def get_plot_name(self, plot_id): widget = self._get_plot_widget(plot_id) @@ -679,7 +683,7 @@ class FlintApi: widget = widgets[iwidget] return widget - def _get_plot_widget(self, plot_id, live_plot=None, custom_plot=None): + def _get_plot_widget(self, plot_id: str, live_plot=None, custom_plot=None): """Get a plot widget (widget while hold a plot) from this `plot_id` Arguments: diff --git a/tests/flint/test_common_plot.py b/tests/flint/test_common_plot.py index 8b0dfb0ece..77609e6ae9 100755 --- a/tests/flint/test_common_plot.py +++ b/tests/flint/test_common_plot.py @@ -5,10 +5,10 @@ from bliss.common import plot def test_empty_plot(flint_session): - p = plot.plot() + p = plot.plot(name="Foo") pid = plot.get_flint()._pid assert "flint_pid={}".format(pid) in repr(p) - assert p.name == "Plot {}".format(p._plot_id) + assert p.name == "Foo" p = plot.plot(name="Some name") assert "flint_pid={}".format(pid) in repr(p) -- GitLab From 0bc9e24b2af1f21ed708caf7c26ee7c22eb159a4 Mon Sep 17 00:00:00 2001 From: Valentin Valls Date: Sun, 6 Dec 2020 22:56:47 +0100 Subject: [PATCH 16/18] Test reuse of plot --- tests/flint/test_custom_plots.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/tests/flint/test_custom_plots.py b/tests/flint/test_custom_plots.py index 4c4afa9c9d..5d352f7168 100644 --- a/tests/flint/test_custom_plots.py +++ b/tests/flint/test_custom_plots.py @@ -40,6 +40,16 @@ def test_custom_plot_curveplot(flint_session): p.clear_data() +def test_reuse_custom_plot(flint_session): + flint = plot.get_flint() + p = flint.get_plot(plot_class="curve", unique_name="foo-reuse") + cos_data = numpy.cos(numpy.linspace(0, 2 * numpy.pi, 10)) + p.add_data({"cos": cos_data}) + p2 = flint.get_plot(plot_class="curve", unique_name="foo-reuse") + data = p2.get_data("cos") + assert data == pytest.approx(cos_data) + + def test_select_points(flint_session): flint = plot.get_flint() p = plot.plot() -- GitLab From a751b37f59f9c96a6aa71909f61cee088bfd5e55 Mon Sep 17 00:00:00 2001 From: Valentin Valls Date: Wed, 9 Dec 2020 21:11:31 +0100 Subject: [PATCH 17/18] Test reusability of plot from common plot API --- tests/flint/test_common_plot.py | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/tests/flint/test_common_plot.py b/tests/flint/test_common_plot.py index 77609e6ae9..11aa01a49b 100755 --- a/tests/flint/test_common_plot.py +++ b/tests/flint/test_common_plot.py @@ -1,6 +1,7 @@ """Testing the BLISS bliss.common.plot API.""" import pytest +import numpy from bliss.common import plot @@ -15,6 +16,26 @@ def test_empty_plot(flint_session): assert p.name == "Some name" +def test_reuse_custom_plot__api_1_0(flint_session): + """Test reuse of custom plot from an ID""" + widget = plot.plot_curve(name="foo") + cos_data = numpy.cos(numpy.linspace(0, 2 * numpy.pi, 10)) + widget.add_data({"cos": cos_data, "foo": cos_data}) + widget2 = plot.plot_curve(name="foo", existing_id=widget.plot_id) + cos = widget2.get_data()["cos"] + numpy.testing.assert_allclose(cos, cos_data) + + +def test_reuse_custom_plot__api_1_6(flint_session): + """Test reuse of custom plot from a name""" + widget = plot.plot_curve(name="foo", existing_id="myplot") + cos_data = numpy.cos(numpy.linspace(0, 2 * numpy.pi, 10)) + widget.add_data({"cos": cos_data, "foo": cos_data}) + widget2 = plot.plot_curve(name="foo", existing_id="myplot") + cos = widget2.get_data()["cos"] + numpy.testing.assert_allclose(cos, cos_data) + + def test_simple_plot(flint_session): sin = flint_session.env_dict["sin_data"] p = plot.plot(sin) -- GitLab From da8450c1dda17d646776262a95b03fc23f4fd052 Mon Sep 17 00:00:00 2001 From: Valentin Valls Date: Wed, 9 Dec 2020 22:11:32 +0100 Subject: [PATCH 18/18] Update curve documentation --- doc/docs/flint/flint_data_plotting.md | 45 ++++++++++++++------------- 1 file changed, 23 insertions(+), 22 deletions(-) diff --git a/doc/docs/flint/flint_data_plotting.md b/doc/docs/flint/flint_data_plotting.md index b32a978361..bcb6edc57b 100644 --- a/doc/docs/flint/flint_data_plotting.md +++ b/doc/docs/flint/flint_data_plotting.md @@ -7,25 +7,39 @@ During a BLISS session users may create data (other than scan data) that needs t The **bliss.common.plot** module offers several types of plot: -### curve plot +### Curve plot `class CurvePlot(BasePlot)` - * used for `ascan`, `a2scan`, etc. * plotting of one or several 1D data as curves * Optional x-axis data can be provided * the plot is created using `plot_curve` -### scatter plot +```python +import numpy +from bliss.common import plot as plot_mdl + +# Function +t = numpy.linspace(0, 10 * numpy.pi, 100) +y = numpy.sin(t) +plot_mdl.plot_curve(data=y, name="My sin") + +# Parametric function +t = numpy.linspace(-3, 3, 50) +x = 16 * numpy.sin(t)**3 +y = 13 * numpy.cos(t) - 5 * numpy.cos(2*t) - 2 * numpy.cos(3*t) - numpy.cos(4*t) +plot_mdl.plot_curve(data=y, x=x, name="My heart") +``` + +### Scatter plot `class ScatterPlot(BasePlot)` - * used for `amesh` scan etc. * plotting one or several scattered data * each scatter is a group of three 1D data of same length * the plot is created using `plot_scatter` -### image plot +### Image plot `class ImagePlot(BasePlot)` @@ -34,7 +48,7 @@ The **bliss.common.plot** module offers several types of plot: * the plot is created using `plot_image` -### image + histogram plot +### Image + histogram plot `class HistogramImagePlot(BasePlot)` @@ -42,16 +56,7 @@ The **bliss.common.plot** module offers several types of plot: * two histograms along the X and Y dimensions are displayed * the plot is created using `plot_image_with_histogram` -### curve list plot - -`CurveListPlot(BasePlot)` - - * plot a single list of 1D data as curves - * a slider and an envelop view are provided - * the plot is created using `plot_curve_list` - * this widget is not integrated yet! - -### image stack plot +### Image stack plot `class ImageStackPlot(BasePlot)` @@ -69,12 +74,8 @@ All the above plot types provide the same interface. They take the data as an argument and return a plot. Here's an example on how to display a cosine wave in a curve plot. ```python -from bliss.common.plot import * -import numpy - xx = numpy.linspace(0, 4*3.14, 50) yy = numpy.cos(xx) - plot(yy, name="Plot 0") ``` @@ -152,11 +153,11 @@ p.clear_data() To sum up, here's how to achieve the same cosine chart of the previous section in a different way: ```python -from bliss.common.plot import * +from bliss.common import plot as plot_mdl import numpy # create plot object -p = bliss.common.plot.CurvePlot() +p = plot_mdl.plot_curve() # create data : x and y values xx = numpy.linspace(0, 4*3.14, 50) -- GitLab